Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions backend/middleware/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,30 @@ async def dispatch(self, request: Request, call_next):
if request.method in _SAFE_METHODS:
return await call_next(request)

origin = _origin_from_header(request)

if origin is None:
# No Origin / Referer — allow (same-origin or non-browser client).
return await call_next(request)

if origin not in self._allowed:
logger.warning(
"CSRF check failed: method=%s path=%s origin=%r not in allowlist",
try:
origin = _origin_from_header(request)

if origin is not None and origin not in self._allowed:
logger.warning(
"CSRF check failed: method=%s path=%s origin=%r not in allowlist",
request.method,
request.url.path,
origin,
)
return JSONResponse(
{"detail": "CSRF check failed: origin not allowed"},
status_code=403,
)
except Exception:
logger.exception(
"Failure recovery triggered in security middleware: method=%s path=%s",
request.method,
request.url.path,
origin,
)
return JSONResponse(
{"detail": "CSRF check failed: origin not allowed"},
status_code=403,
{"detail": "Security verification error: middleware failure recovery triggered"},
status_code=500,
)

return await call_next(request)

24 changes: 24 additions & 0 deletions backend/tests/test_csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,27 @@ def test_plugin_run_attacker_origin_blocked():
headers={"Origin": "https://evil.com"},
)
assert r.status_code == 403


# ── Failure Recovery Tests ───────────────────────────────────────────────────

from unittest.mock import patch


def test_security_middleware_failure_recovery_on_exception():
"""When an exception occurs during origin verification, middleware returns 500 fail-closed."""
with patch("middleware.csrf._origin_from_header", side_effect=RuntimeError("Header extraction fault")):
r = client.post(
"/api/sessions/",
json={"title": "Failure recovery test"},
)
assert r.status_code == 500
assert "Security verification error" in r.json().get("detail", "")


def test_security_middleware_failure_recovery_safe_methods():
"""Safe HTTP methods bypass origin processing and succeed even during header faults."""
with patch("middleware.csrf._origin_from_header", side_effect=RuntimeError("Header extraction fault")):
r = client.get("/api/sessions/")
assert r.status_code == 200

9 changes: 9 additions & 0 deletions docs/csrf-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ This significantly reduces the CSRF surface, but a residual risk exists:
3. **`Referer` fallback** — when `Origin` is absent but `Referer` is present,
the header is normalised to `scheme://host` and checked against the same
list.
4. **Failure recovery** — if an unexpected exception occurs during origin parsing
or validation, the middleware catches the exception, logs an error traceback,
and returns `HTTP 500` (`Security verification error: middleware failure recovery triggered`)
to guarantee a fail-closed security posture.


### Integration: `backend/app.py`

Expand Down Expand Up @@ -115,6 +120,10 @@ requests would break the application when the frontend is served by the same
FastAPI process (production mode). It would also break direct API access from
`curl` and the `pytest` test client — neither of which is a CSRF attack.

### Why Fail-Closed Failure Recovery?

If header processing or origin validation fails unexpectedly (e.g. malformed headers or runtime errors during header inspection), allowing the request to proceed without validation could bypass security checks. Failing closed with an HTTP 500 error response and logging the traceback guarantees that security checks are strictly enforced.

## References

- [OWASP CSRF Prevention Cheat Sheet — Verifying Origin With Standard Headers](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#verifying-origin-with-standard-headers)
Expand Down
Loading