Skip to content

Commit f5b1d44

Browse files
committed
changes from bot review
1 parent 0424116 commit f5b1d44

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

backend/handler/metadata/ss_handler.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re
33
from datetime import datetime
44
from typing import Final, NotRequired, TypedDict
5-
from urllib.parse import quote
5+
from urllib.parse import quote, urlparse
66

77
import pydash
88
from unidecode import unidecode as uc
@@ -34,13 +34,32 @@
3434
SENSITIVE_KEYS = {"ssid", "sspassword"}
3535

3636

37+
def _is_screenscraper_host(url: str) -> bool:
38+
"""True only if the URL's hostname is screenscraper.fr or a subdomain.
39+
40+
Substring matching would let an attacker-controlled host like
41+
screenscraper.fr.evil.example receive the user's credentials.
42+
"""
43+
try:
44+
host = urlparse(url).hostname
45+
except ValueError:
46+
return False
47+
48+
if not host:
49+
return False
50+
51+
return host.lower() == "screenscraper.fr" or host.lower().endswith(
52+
".screenscraper.fr"
53+
)
54+
55+
3756
def add_ss_auth_to_url(url: str | None) -> str:
3857
"""Re-add SS user credentials to a media URL at download time (never stored).
3958
4059
Only injects credentials for screenscraper.fr URLs; returns other URLs
4160
unchanged to avoid leaking credentials to third-party sources.
4261
"""
43-
if not url or "screenscraper.fr" not in url:
62+
if not url or not _is_screenscraper_host(url):
4463
return url or ""
4564

4665
if not SCREENSCRAPER_USER or not SCREENSCRAPER_PASSWORD:

backend/tests/handler/metadata/test_ss_handler.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,50 @@ def test_handles_stripped_url_from_extract_media(self):
397397
assert query.get("devpassword") == ["devpw"]
398398
assert query.get("other") == ["keep"]
399399

400+
def test_rejects_lookalike_and_attacker_hosts(self):
401+
"""Credentials must only be injected when the hostname is exactly
402+
screenscraper.fr or a subdomain. A substring match would leak creds
403+
to attacker-controlled domains."""
404+
hostile_urls = [
405+
# Suffix attack: hostname ends with attacker-controlled domain
406+
"https://screenscraper.fr.evil.example/img.png",
407+
# Substring in path/query of unrelated host
408+
"https://evil.example/?u=screenscraper.fr",
409+
"https://evil.example/screenscraper.fr/img.png",
410+
# Credentials in userinfo pointing at attacker host
411+
"https://screenscraper.fr@evil.example/img.png",
412+
# Prefix attack
413+
"https://notscreenscraper.fr/img.png",
414+
]
415+
with (
416+
patch("handler.metadata.ss_handler.SCREENSCRAPER_USER", "user1"),
417+
patch("handler.metadata.ss_handler.SCREENSCRAPER_PASSWORD", "pw1"),
418+
):
419+
for url in hostile_urls:
420+
result = add_ss_auth_to_url(url)
421+
assert result == url, f"Credentials leaked to {url!r}"
422+
assert "ssid" not in parse_qs(urlparse(result).query)
423+
assert "sspassword" not in parse_qs(urlparse(result).query)
424+
425+
def test_accepts_screenscraper_subdomains(self):
426+
"""Subdomains of screenscraper.fr (e.g. api.screenscraper.fr) are
427+
treated as the same trust boundary and receive credentials."""
428+
urls = [
429+
"https://screenscraper.fr/img.png",
430+
"https://api.screenscraper.fr/api2/foo",
431+
"https://www.screenscraper.fr/img.png",
432+
"https://SCREENSCRAPER.FR/img.png", # case-insensitive host
433+
]
434+
with (
435+
patch("handler.metadata.ss_handler.SCREENSCRAPER_USER", "user1"),
436+
patch("handler.metadata.ss_handler.SCREENSCRAPER_PASSWORD", "pw1"),
437+
):
438+
for url in urls:
439+
result = add_ss_auth_to_url(url)
440+
query = parse_qs(urlparse(result).query)
441+
assert query.get("ssid") == ["user1"], f"Creds missing on {url!r}"
442+
assert query.get("sspassword") == ["pw1"], f"Creds missing on {url!r}"
443+
400444
def test_strip_then_reauth_roundtrip(self):
401445
"""End-to-end: storing media strips user creds; download-time auth
402446
restores them without leaking creds into intermediate state."""

0 commit comments

Comments
 (0)