Skip to content

Commit b1db0b6

Browse files
Merge pull request from GHSA-q764-g6fm-555v
* Improve URL and URI handling * Back to SpotifyException for backward-compatibility * fix copy paste typo * TODO v3 comments Co-authored-by: Stephane Bruckert <[email protected]>
1 parent 262e7a0 commit b1db0b6

File tree

2 files changed

+53
-17
lines changed

2 files changed

+53
-17
lines changed

CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## Unreleased
99

1010
// Add new changes below this line
11-
- Modified docstring for playlist_add_items() to accept "only URIs or URLs",
12-
with intended deprecation for IDs in v3
1311

1412
### Added
1513

1614
- Add alternative module installation instruction to README
1715
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
1816
- Added playlist_add_tracks.py to example folder
1917

18+
### Changed
19+
20+
- Modified docstring for playlist_add_items() to accept "only URIs or URLs",
21+
with intended deprecation for IDs in v3
22+
2023
### Fixed
2124

25+
- Path traversal vulnerability that may lead to type confusion in URI handling code
2226
- Update contributing.md
2327

2428
### Removed

spotipy/client.py

+47-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import json
88
import logging
9+
import re
910
import warnings
1011

1112
import requests
@@ -96,6 +97,29 @@ class Spotify(object):
9697
"US",
9798
"UY"]
9899

100+
# Spotify URI scheme defined in [1], and the ID format as base-62 in [2].
101+
#
102+
# Unfortunately the IANA specification is out of date and doesn't include the new types
103+
# show and episode. Additionally, for the user URI, it does not specify which characters
104+
# are valid for usernames, so the assumption is alphanumeric which coincidentially are also
105+
# the same ones base-62 uses.
106+
# In limited manual exploration this seems to hold true, as newly accounts are assigned an
107+
# identifier that looks like the base-62 of all other IDs, but some older accounts only have
108+
# numbers and even older ones seemed to have been allowed to freely pick this name.
109+
#
110+
# [1] https://www.iana.org/assignments/uri-schemes/prov/spotify
111+
# [2] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
112+
_regex_spotify_uri = r'^spotify:(?P<type>track|artist|album|playlist|show|episode|user):(?P<id>[0-9A-Za-z]+)$'
113+
114+
# Spotify URLs are defined at [1]. The assumption is made that they are all
115+
# pointing to open.spotify.com, so a regex is used to parse them as well,
116+
# instead of a more complex URL parsing function.
117+
#
118+
# [1] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
119+
_regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?P<type>track|artist|album|playlist|show|episode|user)\/(?P<id>[0-9A-Za-z]+)(\?.*)?$'
120+
121+
_regex_base62 = r'^[0-9A-Za-z]+$'
122+
99123
def __init__(
100124
self,
101125
auth=None,
@@ -1940,20 +1964,28 @@ def _append_device_id(self, path, device_id):
19401964
return path
19411965

19421966
def _get_id(self, type, id):
1943-
fields = id.split(":")
1944-
if len(fields) >= 3:
1945-
if type != fields[-2]:
1946-
logger.warning('Expected id of type %s but found type %s %s',
1947-
type, fields[-2], id)
1948-
return fields[-1].split("?")[0]
1949-
fields = id.split("/")
1950-
if len(fields) >= 3:
1951-
itype = fields[-2]
1952-
if type != itype:
1953-
logger.warning('Expected id of type %s but found type %s %s',
1954-
type, itype, id)
1955-
return fields[-1].split("?")[0]
1956-
return id
1967+
uri_match = re.search(Spotify._regex_spotify_uri, id)
1968+
if uri_match is not None:
1969+
uri_match_groups = uri_match.groupdict()
1970+
if uri_match_groups['type'] != type:
1971+
# TODO change to a ValueError in v3
1972+
raise SpotifyException(400, -1, "Unexpected Spotify URI type.")
1973+
return uri_match_groups['id']
1974+
1975+
url_match = re.search(Spotify._regex_spotify_url, id)
1976+
if url_match is not None:
1977+
url_match_groups = url_match.groupdict()
1978+
if url_match_groups['type'] != type:
1979+
raise SpotifyException(400, -1, "Unexpected Spotify URL type.")
1980+
# TODO change to a ValueError in v3
1981+
return url_match_groups['id']
1982+
1983+
# Raw identifiers might be passed, ensure they are also base-62
1984+
if re.search(Spotify._regex_base62, id) is not None:
1985+
return id
1986+
1987+
# TODO change to a ValueError in v3
1988+
raise SpotifyException(400, -1, "Unsupported URL / URI.")
19571989

19581990
def _get_uri(self, type, id):
19591991
if self._is_uri(id):
@@ -1962,7 +1994,7 @@ def _get_uri(self, type, id):
19621994
return "spotify:" + type + ":" + self._get_id(type, id)
19631995

19641996
def _is_uri(self, uri):
1965-
return uri.startswith("spotify:") and len(uri.split(':')) == 3
1997+
return re.search(Spotify._regex_spotify_uri, uri) is not None
19661998

19671999
def _search_multiple_markets(self, q, limit, offset, type, markets, total):
19682000
if total and limit > total:

0 commit comments

Comments
 (0)