CSRF Protection
How it works
- Token generated automatically for each session
- Session-side synchronizer token pattern: the token is stored server-side in the session, exposed only through a hidden field or the
X-CSRF-Tokenheader — no separate JS-readable cookie (the session cookie itself isHttpOnly) - Verified on POST, PUT, PATCH, DELETE requests
- Ignored on GET, HEAD, OPTIONS requests
In Runique forms
When you use {% form.xxx %}, CSRF is included automatically. No need to add it manually.
In manual HTML forms
<form method="post" action="/submit">
{% csrf %}
<input type="text" name="data">
<button type="submit">Send</button>
</form>
For AJAX requests
const csrfToken = document.querySelector('[name="csrf_token"]').value;
fetch('/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
Token expiry & automatic refresh
The CSRF token lives in the session. Anonymous sessions (visitor not logged in) expire
by default after 5 minutes of inactivity (with_anonymous_session_duration, see
Sessions) — a tab left open longer on a public form
(/login, /register, etc.) then carries a stale token by the time it's submitted.
Runique handles this at two levels:
- Explicit message: a CSRF failure sets a global error on the form (i18n key
csrf.invalid_or_missing), rendered via{% messages %}— never a silent failure. - Refresh before submission:
js/csrf.jsintercepts every submission of a form carrying acsrf_tokenfield, does a lightweight GET to the current page to fetch a fresh token (every response carries anX-CSRF-Tokenheader), updates the field, then submits — works even if the session fully expired (the server just issues a new one). The user never loses what they typed, and only sees the CSRF failure if the network is down at the moment of submission.
This script is not loaded automatically in an application's templates — include it explicitly in your base layout:
<script {% csp %} src="{{ "js/csrf.js" | runique_static }}" defer></script>
(the admin panel already loads it itself, via runique_static).
Exempt paths (webhooks, APIs)
Some endpoints receive POST requests without a CSRF token — Stripe webhooks, third-party callbacks, JSON APIs called by other servers.
Use .csrf_exempt() to bypass CSRF validation on specific paths:
.middleware(|m| {
m.csrf_exempt(vec!["/webhook/stripe", "/api/callback"])
})
CSRF is always on — there is no .with_csrf() method to call, only the exemption exists.
Matching is exact — /webhook/stripe does not exempt /webhook/stripe/sub.
After exempting a path, verify the request authenticity by other means in your handler (e.g.
Stripe-SignatureHMAC-SHA256 for Stripe webhooks).
csrf_exempt() only skips validation — the CSRF token is still generated and available, so a handler on an exempt path can still use Request/RuniqueContext normally (session, template context, …) alongside its own verification logic.