Skip to content

Commit d4bdfef

Browse files
authored
Merge pull request #3703 from rommapp/posthog-code/fix-romfile-visibility-bypass
fix(roms): enforce parent ROM visibility on direct file endpoints
2 parents e483127 + 3abd082 commit d4bdfef

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

backend/endpoints/roms/files.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from decorators.auth import protected_route
1111
from endpoints.responses.rom import RomFileSchema
1212
from handler.auth.constants import Scope
13+
from handler.auth.dependencies import assert_rom_visible
1314
from handler.database import db_rom_handler
1415
from handler.filesystem import fs_rom_handler
1516
from logger.formatter import BLUE
@@ -42,6 +43,16 @@ async def get_romfile(
4243
detail="File not found",
4344
)
4445

46+
# Resolve back to the parent rom and enforce its visibility, so a file
47+
# belonging to a hidden rom can't be read by direct RomFile.id.
48+
rom = db_rom_handler.get_rom(file.rom_id)
49+
if not rom:
50+
raise HTTPException(
51+
status_code=status.HTTP_404_NOT_FOUND,
52+
detail="File not found",
53+
)
54+
assert_rom_visible(request, rom, not_found_detail="File not found")
55+
4556
return RomFileSchema.model_validate(file)
4657

4758

@@ -69,6 +80,16 @@ async def get_romfile_content(
6980
detail="File not found",
7081
)
7182

83+
# 404-mask file bytes of roms hidden from the caller: resolve the parent
84+
# rom and apply its visibility before serving any content.
85+
rom = db_rom_handler.get_rom(file.rom_id)
86+
if not rom:
87+
raise HTTPException(
88+
status_code=status.HTTP_404_NOT_FOUND,
89+
detail="File not found",
90+
)
91+
assert_rom_visible(request, rom, not_found_detail="File not found")
92+
7293
log.info(
7394
f"User {hl(current_username, color=BLUE)} is downloading {hl(file.file_name)}"
7495
)

backend/tests/endpoints/roms/test_rom.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from config.config_manager import MetadataMediaType
88
from handler.database import db_collection_handler, db_rom_handler
9+
from handler.database.base_handler import sync_session
910
from handler.filesystem.resources_handler import FSResourcesHandler
1011
from handler.filesystem.roms_handler import FSRomsHandler
1112
from handler.metadata.flashpoint_handler import FlashpointHandler, FlashpointRom
@@ -16,6 +17,7 @@
1617
from handler.metadata.ra_handler import RAGameRom, RAHandler
1718
from handler.metadata.ss_handler import SSHandler, SSRom
1819
from models.collection import Collection
20+
from models.permission import HiddenEntity, PermEntity
1921
from models.platform import Platform
2022
from models.rom import Rom, RomFile, compute_name_sort_key
2123
from models.user import User
@@ -318,6 +320,50 @@ def test_get_rom_content_missing_rom_returns_404(client: TestClient, access_toke
318320
assert response.status_code == status.HTTP_404_NOT_FOUND
319321

320322

323+
def _hide_rom_for_user(rom_id: int, user_id: int) -> None:
324+
with sync_session.begin() as s:
325+
s.add(HiddenEntity(entity=PermEntity.ROMS, entity_id=rom_id, user_id=user_id))
326+
327+
328+
def test_get_romfile_content_visible_rom(
329+
client: TestClient, viewer_access_token: str, rom: Rom, rom_file
330+
):
331+
# Baseline: a rom the viewer can see is downloadable by direct RomFile.id.
332+
response = client.get(
333+
f"/api/roms/{rom_file.id}/files/content/whatever.bin",
334+
headers={"Authorization": f"Bearer {viewer_access_token}"},
335+
follow_redirects=False,
336+
)
337+
assert response.status_code == status.HTTP_200_OK
338+
assert "X-Accel-Redirect" in response.headers
339+
340+
341+
def test_get_romfile_content_hidden_rom_returns_404(
342+
client: TestClient, viewer_access_token: str, viewer_user, rom: Rom, rom_file
343+
):
344+
# A file belonging to a hidden rom must 404 even by direct RomFile.id,
345+
# matching the ROM-level content endpoint (visibility-bypass regression).
346+
_hide_rom_for_user(rom.id, viewer_user.id)
347+
response = client.get(
348+
f"/api/roms/{rom_file.id}/files/content/whatever.bin",
349+
headers={"Authorization": f"Bearer {viewer_access_token}"},
350+
follow_redirects=False,
351+
)
352+
assert response.status_code == status.HTTP_404_NOT_FOUND
353+
354+
355+
def test_get_romfile_hidden_rom_returns_404(
356+
client: TestClient, viewer_access_token: str, viewer_user, rom: Rom, rom_file
357+
):
358+
# The file metadata endpoint must not leak files of a hidden rom either.
359+
_hide_rom_for_user(rom.id, viewer_user.id)
360+
response = client.get(
361+
f"/api/roms/{rom_file.id}/files",
362+
headers={"Authorization": f"Bearer {viewer_access_token}"},
363+
)
364+
assert response.status_code == status.HTTP_404_NOT_FOUND
365+
366+
321367
@patch.object(FSRomsHandler, "rename_fs_rom")
322368
@patch.object(IGDBHandler, "get_rom_by_id", return_value=IGDBRom(igdb_id=None))
323369
def test_update_rom(

0 commit comments

Comments
 (0)