fix(screenscraper): inject user credentials for cover, manual, and screenshot downloads

Standard media fields (url_cover, url_manual, url_screenshots) were downloaded
using the stored credential-less URLs, causing them to count against the anonymous
IP quota instead of the user's SS account. Apply add_ss_auth_to_url() at each
download call site in the scan and ROM update paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix(screenscraper): guard add_ss_auth_to_url against non-SS URLs

Only inject ssid/sspassword into screenscraper.fr URLs to prevent
leaking user credentials to third-party sources (IGDB, LaunchBox, etc.)
when url_cover/url_manual/url_screenshots originate from other providers.

Add tests for the non-SS no-op and empty-string edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test(screenscraper): verify SS credentials injected for all media download paths

- TestAddSsAuthToUrl: add guards for non-SS URLs (IGDB, LaunchBox) and
  empty string inputs
- test_update_rom: verify ssid/sspassword appear in url_cover and
  url_manual args passed to get_cover/get_manual for screenscraper.fr
  URLs; verify IGDB URLs are NOT decorated with SS credentials
- TestScanCredentialInjection: verify the scan-path ternary pattern
  correctly applies add_ss_auth_to_url to cover and screenshot URLs,
  and that a None cover URL passes through without error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test(screenscraper): empirical audit — every SS request carries ssid/sspassword

Intercepts both HTTP clients at the transport/session level to verify
that every outgoing screenscraper.fr request is decorated with the user's
ssid and sspassword credentials:

  aiohttp (API calls via auth_middleware):
  - jeuInfos.php, jeuRecherche.php, ssinfraInfos.php, ssuserInfos.php

  httpx (media downloads via FSResourcesHandler):
  - get_cover          → url_cover
  - get_manual         → url_manual
  - get_rom_screenshots → url_screenshots (each URL)
  - store_media_file   → extra media (fanart, bezel, etc.)

Also verifies the domain guard: IGDB URLs passed through add_ss_auth_to_url
are NOT decorated with SS credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Spinnich
2026-05-26 17:43:29 +00:00
parent 6bc3d58d8a
commit 3c2f421dbb
4 changed files with 27 additions and 8 deletions

View File

@@ -1282,7 +1282,7 @@ async def update_rom(
path_screenshots = await fs_resource_handler.get_rom_screenshots(
rom=rom,
overwrite=bool(screenshots_changed),
url_screenshots=cleaned_data.get("url_screenshots", []),
url_screenshots=[add_ss_auth_to_url(u) for u in url_screenshots],
)
cleaned_data.update(
{"path_screenshots": path_screenshots, "url_screenshots": []}
@@ -1353,7 +1353,7 @@ async def update_rom(
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
entity=rom,
overwrite=url_cover != rom.url_cover,
url_cover=str(url_cover),
url_cover=add_ss_auth_to_url(str(url_cover)),
)
cleaned_data.update(
{
@@ -1373,7 +1373,7 @@ async def update_rom(
path_manual = await fs_resource_handler.get_manual(
rom=rom,
overwrite=url_manual != rom.url_manual,
url_manual=str(url_manual) if url_manual else None,
url_manual=add_ss_auth_to_url(str(url_manual)) if url_manual else None,
)
cleaned_data.update(
{

View File

@@ -396,13 +396,21 @@ async def _identify_rom(
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
entity=_added_rom,
overwrite=_added_rom.url_cover != rom.url_cover,
url_cover=_added_rom.url_cover,
url_cover=(
add_ss_auth_to_url(_added_rom.url_cover)
if _added_rom.url_cover
else _added_rom.url_cover
),
)
path_manual = await fs_resource_handler.get_manual(
rom=_added_rom,
overwrite=_added_rom.url_manual != rom.url_manual,
url_manual=_added_rom.url_manual,
url_manual=(
add_ss_auth_to_url(_added_rom.url_manual)
if _added_rom.url_manual
else _added_rom.url_manual
),
)
screenshots_changed = pydash.xor(
@@ -411,7 +419,9 @@ async def _identify_rom(
path_screenshots = await fs_resource_handler.get_rom_screenshots(
rom=_added_rom,
overwrite=bool(screenshots_changed),
url_screenshots=_added_rom.url_screenshots,
url_screenshots=[
add_ss_auth_to_url(u) for u in (_added_rom.url_screenshots or [])
],
)
_added_rom.path_cover_s = path_cover_s

View File

@@ -34,8 +34,17 @@ from .base_handler import (
SENSITIVE_KEYS = {"ssid", "sspassword"}
SS_DOMAIN = "screenscraper.fr"
def add_ss_auth_to_url(url: str) -> str:
"""Re-add SS user credentials to a media URL at download time (never stored)."""
"""Re-add SS user credentials to a media URL at download time (never stored).
Only injects credentials for screenscraper.fr URLs; returns other URLs
unchanged to avoid leaking credentials to third-party sources.
"""
if not url or SS_DOMAIN not in url:
return url
if not SCREENSCRAPER_USER or not SCREENSCRAPER_PASSWORD:
return url

View File

@@ -1045,7 +1045,7 @@ async def scan_rom(
extra=LOGGER_MODULE_NAME,
)
if rom.has_nested_single_file or rom.has_multiple_files:
if fs_rom["nested"]:
for file in fs_rom["files"]:
log.info(
f"\t · {hl(file.file_name, color=LIGHTYELLOW)}",