Conversation
There was a problem hiding this comment.
Pull request overview
Implements CSRF support for Jinja template workflows in Air by introducing a shared CSRF utility module, adding a new CSRFMiddleware for double-submit cookie validation, and exposing template helpers to render/read CSRF tokens in Jinja templates.
Changes:
- Added
CSRFMiddlewareto set/validate CSRF cookies and verify submitted tokens (form field orX-CSRF-Tokenheader). - Added
csrf.pyutilities and wired Jinja globals (csrf_token(),csrf_input()) into the template environment. - Added Jinja + middleware tests and a test template to validate end-to-end behavior.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Bumps ty dev dependency version. |
| src/air/csrf.py | New shared CSRF constants + request/scope helpers. |
| src/air/middleware.py | Adds CSRFMiddleware implementing double-submit cookie CSRF validation. |
| src/air/templating.py | Registers Jinja globals for CSRF token + hidden input rendering. |
| src/air/init.py | Re-exports CSRFMiddleware from the package root. |
| tests/test_templating.py | Adds test asserting CSRF helpers render expected token/hidden input. |
| tests/test_middleware.py | Adds integration-style tests for valid/missing/invalid/header CSRF submissions. |
| tests/templates/csrf_form.html | New template exercising csrf_input() and csrf_token() globals. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if submitted_token is None: | ||
| buffered_body = await _read_body(receive) | ||
| submitted_token = await self._extract_form_token(scope, headers, buffered_body) | ||
| downstream_receive = _receive_from_body(buffered_body) |
There was a problem hiding this comment.
CSRFMiddleware buffers the entire request body into memory when the CSRF header is missing (_read_body + _extract_form_token). For large multipart/form-data submissions (e.g., file uploads), this can cause unbounded memory usage and makes a straightforward DoS vector. Consider enforcing a maximum buffered size (via Content-Length + incremental limit), or requiring the header token for multipart requests and skipping body buffering in that case, returning 413/403 when the body is too large to safely buffer.
| cookie_samesite: Literal["lax", "strict", "none"] = "lax", | ||
| cookie_max_age: int | None = None, | ||
| error_message: str = "CSRF verification failed.", | ||
| ) -> None: |
There was a problem hiding this comment.
SameSite=None cookies are rejected by modern browsers unless they also have the Secure attribute. Since this middleware exposes cookie_samesite and cookie_secure independently, setting cookie_samesite="none" with cookie_secure=False will silently break CSRF (cookie never stored/sent). Consider validating these options in __init__ and raising a ValueError (or auto-enabling cookie_secure) when cookie_samesite == "none" and cookie_secure is false.
| ) -> None: | |
| ) -> None: | |
| if cookie_samesite == "none" and not cookie_secure: | |
| raise ValueError( | |
| 'CSRFMiddleware: cookie_samesite="none" requires cookie_secure=True ' | |
| "for modern browsers to accept the CSRF cookie." | |
| ) |
What
CSRFMiddlewareFixes issue #110
Note: This is a bit complex, will compare with @Isaac-Flath's https://github.com/kentro-tech/aircsrf/blob/main/src/air_csrf/middleware.py and consider a rewrite.
AI Provenance (omit for human‑authored PRs)
Used AI to help write tests and address some typing issues
Pattern
This adds a
csrf.pyutility module and otherwise touchesmiddleware.pyandtemplating.py.Reviewer Focus
CSRFMiddlewareis not configuredChecklist