fix: resolve endpoint before execution in CSRF middleware (#626)
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:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user