"""Regression test for the SPA static-file path-traversal fix. Before the fix, the `/{path:path}` catch-all joined the request path onto the static dir with no containment check, so percent-encoded traversal (GET /..%2f..%2fdata%2fportal.db) read arbitrary files off disk — including the session secret, which allowed forging an admin session. See main.py. """ import tempfile from pathlib import Path from fastapi import FastAPI from fastapi.responses import FileResponse from fastapi.testclient import TestClient from ten31portal.main import _contained_static_path def test_contained_helper_blocks_traversal(): root = Path(tempfile.mkdtemp()) static = root / "static" static.mkdir() (static / "index.html").write_text("spa") (static / "app.js").write_text("ok") data = root / "data" data.mkdir() (data / "portal.db").write_text("secret-db") # Legit assets resolve within the root. assert _contained_static_path(static, "app.js") == (static / "app.js").resolve() assert _contained_static_path(static, "index.html") == (static / "index.html").resolve() # Traversal (already-decoded, i.e. what ..%2f becomes) escapes -> None. for evil in ("../data/portal.db", "../../data/portal.db", "../data/../data/portal.db"): assert _contained_static_path(static, evil) is None, evil def test_spa_route_does_not_leak_via_encoded_traversal(): """End-to-end: encoded traversal against the real route shape returns the SPA shell, never the out-of-root file.""" root = Path(tempfile.mkdtemp()) static = root / "static" static.mkdir() (static / "index.html").write_text("SPA") data = root / "data" data.mkdir() (data / ".session-secret").write_text("TOPSECRET") app = FastAPI() def _index(): return FileResponse(static / "index.html") @app.api_route("/{path:path}", methods=["GET", "HEAD"]) async def serve_spa(path: str): file = _contained_static_path(static, path) if file is None or not file.is_file(): return _index() return FileResponse(file) client = TestClient(app) for attack in ( "/..%2f..%2fdata%2f.session-secret", "/%2e%2e%2f%2e%2e%2fdata%2f.session-secret", "/../../data/.session-secret", ): r = client.get(attack) assert "TOPSECRET" not in r.text, attack