Skip to content

Reject cross-site POSTs to state-changing API routes#14

Merged
urmeo merged 3 commits into
mainfrom
fix/csrf-same-site-guard
Jul 7, 2026
Merged

Reject cross-site POSTs to state-changing API routes#14
urmeo merged 3 commits into
mainfrom
fix/csrf-same-site-guard

Conversation

@urmeo

@urmeo urmeo commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

In no-key local mode the API is open by design, but that also means a web page the user merely visits can submit an HTML <form> to http://127.0.0.1:8000/api/ingest and poison the local corpus. A form POST is a CORS simple request — it runs with no preflight, so the CORS config never blocks the side effect (it only blocks the attacker from reading the response). Classic CSRF.

Fix

New require_same_site dependency on the four state-changing routes (POST /ingest, POST /query, DELETE /documents, DELETE /documents/{id}). It trusts the browser-set Sec-Fetch-Site header (rejects cross-site), falls back to an Origin allowlist for older browsers, and allows requests carrying neither header (curl, the CLI, server-to-server) since those have no ambient-credential CSRF surface.

Verification

  • 6 new unit tests in test_security.py (cross-site rejected, same-origin/none/no-header allowed, Origin fallback both ways).
  • End-to-end through TestClient: cross-site form POST to /api/ingest → 403; same-origin → 200.
  • Full suite green: 27 security + 62 route tests pass.

Closes #13

@urmeo
urmeo merged commit 4edb43f into main Jul 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No-key mode accepts cross-site POSTs to /api/ingest

1 participant