Security
axion ships built-in CSRF and CORS middleware, plus sensible default headers on every response.
CSRF protection
CSRF protection is enabled by default (app.enable_csrf = True). It is
implemented by the built-in CSRFMiddleware, which runs for every request:
- Safe methods (
GET,HEAD,OPTIONS,TRACE) always pass, and a freshcsrftokencookie is issued onGETresponses so the browser holds a token. -
Unsafe methods (
POST,PUT,PATCH,DELETE) are verified using one of three strategies:- Explicit header — the
X-CSRFTokenheader must match thecsrftokencookie (double-submit cookie pattern). - Form body — a
csrfmiddlewaretokenfield in anapplication/x-www-form-urlencodedbody must match the cookie. This covers traditional HTML form submissions. - Origin / Referer — the
OriginorRefererheader is checked againstapp.allowed_hosts. The cookie must also be present. This covers browser requests where the header is sent automatically.
If none of these succeed, a
403is returned. - Explicit header — the
Disable it for pure APIs or during development:
app = WebServer(debug=True)
app.enable_csrf = False
Configure the cookie and header names in settings:
# config/settings.py
CSRF_COOKIE_NAME = "csrftoken"
CSRF_HEADER_NAME = "X-CSRFToken"
Sending a token from JavaScript
Browser requests — the cookie is sent automatically; as long as
Origin/Referer matches ALLOWED_HOSTS (the default ["*"] allows
everything), no extra work is needed:
fetch('/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
Explicit header — for non-browser clients or when you need full control, read the cookie and send it back on the header:
const token = document.cookie
.split('; ')
.find((row) => row.startsWith('csrftoken='))
?.split('=')[1];
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': token,
},
body: JSON.stringify({ name: 'Alice' }),
});
HTML forms
For server-rendered forms, use the csrf_field() Jinja2 global to embed
the token as a hidden input. The form body is validated automatically:
<form method="post" action="/submit">
<input type="text" name="name">
<button type="submit">Submit</button>
</form>
Low-level helpers
The axion.middleware.csrf module exposes the primitives used by the
middleware, useful for testing or custom flows:
| Helper | Description |
|---|---|
get_token() |
Generate a new CSRF token |
csrf_field() |
Return <input type="hidden"> HTML for use in templates |
csrf_field_with_token(token) |
Same but with an explicit token value |
verify_csrf(request, app=None) |
Validate the token for unsafe methods |
set_csrf_cookie(response, token) |
Attach the csrftoken cookie |
CORS
The built-in CORSMiddleware applies Cross-Origin Resource Sharing headers
when the request’s Origin is allowed, and answers preflight OPTIONS
requests with a 204.
Allowed origins default to ["*"] (allow all):
# config/settings.py
CORS_ALLOWED_ORIGINS = ["https://example.com"]
CORS_ALLOW_CREDENTIALS = True
Set them per-app at runtime:
app = WebServer(debug=True)
app.cors_origins = ["https://example.com", "http://localhost:5173"]
When a request has an allowed Origin, the response includes
Access-Control-Allow-Origin, -Methods, -Headers, and -Credentials.
Default response headers
Every Response starts with:
X-Content-Type-Options: nosniff
and you can add more in a hook-object middleware (see the middleware guide):
from axion.middleware import BaseMiddleware
class SecurityHeadersMiddleware(BaseMiddleware):
def before_request(self, request, response):
response.set_header("X-Frame-Options", "DENY")
response.set_header("Referrer-Policy", "strict-origin-when-cross-origin")
Host validation
Every request’s Host header is validated against ALLOWED_HOSTS before
anything else runs. This blocks DNS-rebinding attacks. "*" (the default)
allows any host:
# config/settings.py
ALLOWED_HOSTS = ["example.com", "www.example.com"]
Requests whose hostname (the Host header without the port) is not listed
receive a 400. Set it per-app at runtime:
app = WebServer(debug=True)
app.allowed_hosts = ["example.com"]
Runtime settings
| Setting | Default | Purpose |
|---|---|---|
ALLOWED_HOSTS |
["*"] |
Allowed host header values |
SECRET_KEY |
"change-me" |
Secret used for signing (set in production!) |
CORS_ALLOWED_ORIGINS |
["*"] |
Origins allowed for CORS |
CORS_ALLOW_CREDENTIALS |
True |
Send Access-Control-Allow-Credentials |
CSRF_COOKIE_NAME |
"csrftoken" |
CSRF cookie name |
CSRF_HEADER_NAME |
"X-CSRFToken" |
CSRF header name |
Next steps
- Configure every setting in the configuration reference.
- See the example app for a working CSRF-protected API client flow.