From e09064fd2cd5f32b233bacfbe331c55ff783a65d Mon Sep 17 00:00:00 2001 From: Ian Alloway Date: Tue, 14 Jul 2026 14:17:04 -0400 Subject: [PATCH] fix: protect brief API routes --- solvent/server.py | 21 +++++++++++++-------- tests/test_server.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/solvent/server.py b/solvent/server.py index db823e4..6172ad4 100644 --- a/solvent/server.py +++ b/solvent/server.py @@ -232,6 +232,12 @@ def get_job(job_id: str): metrics = agent.t.get_metrics(job_id) return {"job": dict(row), "metrics": metrics} + def _is_local_request(request: Request | None) -> bool: + if request is None: + return False + client_host = getattr(request.client, "host", "") if request.client else "" + return client_host in ("127.0.0.1", "::1", "localhost", "testclient") + @app.get("/api/receipt/{job_id}") def get_receipt(job_id: str, token: str = "", request: Request = None): """Return a plaintext job receipt. @@ -246,12 +252,7 @@ def get_receipt(job_id: str, token: str = "", request: Request = None): raise HTTPException(404, "job not found") # Auth: localhost OR valid delivery token - is_local = False - if request is not None: - client_host = getattr(request.client, "host", "") if request.client else "" - is_local = client_host in ("127.0.0.1", "::1", "localhost", "testclient") - - if not is_local: + if not _is_local_request(request): if not verify_delivery_token(job_id, token): raise HTTPException(403, "invalid or expired delivery token") @@ -309,7 +310,9 @@ def get_brief(job_id: str, token: str = ""): raise HTTPException(404, "brief not found") @app.get("/api/briefs") - def list_briefs(): + def list_briefs(request: Request = None): + if not _is_local_request(request): + raise HTTPException(403, "brief listing is only available locally") reports_dir = reports_dir_fn() if not reports_dir.is_dir(): return [] @@ -320,7 +323,9 @@ def list_briefs(): return sorted(list(stems)) @app.get("/api/briefs/{job_id}") - def get_brief_api(job_id: str): + def get_brief_api(job_id: str, token: str = "", request: Request = None): + if not _is_local_request(request) and not verify_delivery_token(job_id, token): + raise HTTPException(403, "invalid or expired delivery token") reports_dir = reports_dir_fn().resolve() path_html = (reports_dir / f"{job_id}.html").resolve() diff --git a/tests/test_server.py b/tests/test_server.py index 836a244..199d33e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -38,6 +38,31 @@ def test_brief_serving_requires_exact_job_id_match(self): self.assertEqual(response.status_code, 200) self.assertIn("Private brief", response.text) + def test_api_brief_routes_require_localhost_or_delivery_token(self): + try: + from fastapi.testclient import TestClient + except ImportError: + self.skipTest("FastAPI test client is not installed") + + client = TestClient(create_app()) + + listed = client.get("/api/briefs") + self.assertEqual(listed.status_code, 200) + self.assertIn("victim-job", listed.json()) + + response = client.get("/api/briefs/victim-job") + self.assertEqual(response.status_code, 200) + self.assertIn("Private brief", response.text) + + remote_client = TestClient(create_app(), client=("203.0.113.10", 12345)) + self.assertEqual(remote_client.get("/api/briefs").status_code, 403) + self.assertEqual(remote_client.get("/api/briefs/victim-job").status_code, 403) + + token = make_delivery_token("victim-job") + response = remote_client.get(f"/api/briefs/victim-job?token={token}") + self.assertEqual(response.status_code, 200) + self.assertIn("Private brief", response.text) + def test_interactive_dashboard_routes(self): try: from fastapi.testclient import TestClient