@@ -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