fix: resolve endpoint before execution in CSRF middleware (#626)
Some checks failed
Tests / lint (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled

Previously, when CSRF validation failed and the path wasn't in the exempt
list, the middleware called call_next() to execute the endpoint BEFORE
checking the @csrf_exempt decorator. This caused side effects (DB writes,
API calls, etc.) to occur on protected endpoints even when CSRF validation
failed.

Now the middleware resolves the route endpoint by walking the FastAPI/
Starlette router WITHOUT executing it, checks @csrf_exempt, and only
then either allows the request through or returns 403.

- Add _resolve_endpoint() method to walk middleware chain and match routes
- Remove call_next() before @csrf_exempt check (5 lines deleted)
- Add regression test proving endpoints don't execute before CSRF check
- Add test confirming @csrf_exempt endpoints still execute normally
This commit is contained in:
hermes
2026-03-20 19:04:52 -04:00
parent d2a5866650
commit 2a4f6228c7
2 changed files with 85 additions and 8 deletions

View File

@@ -120,3 +120,50 @@ class TestCSRFDecoratorSupport:
# Protected endpoint should be 403
response2 = client.post("/protected")
assert response2.status_code == 403
def test_csrf_exempt_endpoint_not_executed_before_check(self):
"""Regression test for #626: endpoint must NOT execute before CSRF check.
Previously the middleware called call_next() first, executing the endpoint
and its side effects, then checked @csrf_exempt afterward. This meant
non-exempt endpoints would execute even when CSRF validation failed.
"""
app = FastAPI()
app.add_middleware(CSRFMiddleware)
side_effect_log: list[str] = []
@app.post("/protected-with-side-effects")
def protected_with_side_effects():
side_effect_log.append("executed")
return {"message": "should not run"}
client = TestClient(app)
# POST without CSRF token — should be blocked with 403
response = client.post("/protected-with-side-effects")
assert response.status_code == 403
# The critical assertion: the endpoint must NOT have executed
assert side_effect_log == [], (
"Endpoint executed before CSRF validation! Side effects occurred "
"despite CSRF failure (see issue #626)."
)
def test_csrf_exempt_endpoint_does_execute(self):
"""Ensure @csrf_exempt endpoints still execute normally."""
app = FastAPI()
app.add_middleware(CSRFMiddleware)
side_effect_log: list[str] = []
@app.post("/exempt-webhook")
@csrf_exempt
def exempt_webhook():
side_effect_log.append("executed")
return {"message": "webhook ok"}
client = TestClient(app)
response = client.post("/exempt-webhook")
assert response.status_code == 200
assert side_effect_log == ["executed"]