CSRF
Set up CSRF protection with browser signals, session binding, and token verification.
CSRF protection is disabled by default. Choose your application's session model first, then choose the CSRF profile to use.
Choose a profile#
| Profile | Checks | Application must provide |
|---|---|---|
cross_origin() |
Origin and Fetch Metadata | Browser signals |
signed_double_submit(secret, binding) |
A session-bound signed token | Secret and binding |
defense_in_depth(secret, binding) |
Origin and signed token | Secret and binding |
synchronizer(secret, binding, store) |
A server-side token | A CsrfTokenStore implementation |
synchronizer_defense_in_depth(...) |
Origin and server-side token | Binding, secret, and store |
With cross_origin(), an unsafe request can be accepted when both Origin and Sec-Fetch-Site are absent. Choose a token-based profile if your application requires token verification.
Session binding and setup#
Place the session ID verified by session middleware in request-local state using CsrfSessionId::new(...). In the following composition snippet, the application supplies the secret and middleware:
let policy = CsrfPolicy::defense_in_depth(
CsrfSecret::new(application_secret)?,
CsrfRequestLocalBinding::new(),
)
.trust_origins(["https://app.example.com"])
.token_header("x-csrf-token")
.token_ttl(std::time::Duration::from_secs(3600));
let builder = AppBuilder::new("127.0.0.1:8080")
.session_middleware::<ApplicationSessionMiddleware>()
.csrf(policy);Alternatively, CsrfSessionCookieBinding::new(name) uses the cookie value as the binding. This path does not automatically consult the session store to verify that the session is valid.
Global or route scope#
.csrf(policy) applies global protection to unsafe requests; exact bypass(method, path) exceptions can be configured. .csrf_route_scoped(policy) protects only actions marked with #[guard(CsrfGuard)]. A route guard that publishes the session must precede CsrfGuard.
The two modes cannot be combined. CsrfGuard without a policy, CsrfGuard with global mode, or a route-scoped setup without any CsrfGuard causes a build error.
Token endpoint#
The policy and session binding must be configured first. Add serde_json = "1". This controller returns the token as JSON:
use std::sync::Arc;
use lilyrs::http_api::{
controller, Controller, ControllerInitError, ControllerTrait, CsrfService,
Extensions, HttpApiError, Request, Response, Service,
};
#[derive(Controller)]
#[base_path("/security")]
struct SecurityController;
#[lilyrs::http_api::async_trait::async_trait]
impl ControllerTrait for SecurityController {
async fn new(_: Arc<Extensions>) -> Result<Self, ControllerInitError> { Ok(Self) }
}
#[controller]
impl SecurityController {
#[get("/csrf")]
async fn csrf_token(
&self, Service(csrf): Service<CsrfService>,
request: &mut Request, response: &mut Response,
) -> Result<(), HttpApiError> {
let token = csrf.issue(request, response).await.map_err(|_| {
HttpApiError::ServiceUnavailable("CSRF token unavailable".into())
})?;
response.try_insert_header("content-type", "application/json")
.map_err(|_| HttpApiError::ResponseEncodingError("header rejected".into()))?;
response.try_insert_header("cache-control", "no-store")
.map_err(|_| HttpApiError::ResponseEncodingError("header rejected".into()))?;
let body = serde_json::to_vec(&serde_json::json!({ "token": token.as_str() }))
.map_err(|_| HttpApiError::ResponseEncodingError("JSON failed".into()))?;
response.set_body_vec(body)?;
Ok(())
}
}The manual response preserves the Set-Cookie written by the signed profile. The browser reads the token from JSON and sends it in the x-csrf-token header on unsafe requests. JavaScript does not read the HTTP-only cookie. For cross-origin flows, also configure CORS credentials and the browser's credentials setting.
Rotation and storage#
Options include rotate, clear, previous_verification_key, cookie_policy, token_ttl, and allow_form_field for URL-encoded forms. In a signed profile, issue a token for the new binding when the session ID changes. In a synchronizer profile, clear the old binding's state before changing the ID.
CsrfTokenStore must implement the atomicity contract for load-or-insert, rotate, and revoke operations. With multiple instances, a map held only in process memory is not a sufficient shared store.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.