Security plugin
Automatic CSRF protection and a hardening-headers bundle. Mount it and you're done.
umbral-security gives every non-safe request automatic CSRF validation (signed double-submit) plus a modern security-header bundle. Mount it and you're done: the middleware mints the token before your handler runs, your templates receive it ambiently, and a missing or forged token on any POST / PUT / PATCH / DELETE returns 403. Your view code never touches CSRF.
App::builder() .plugin(AuthPlugin::default()) .plugin(SecurityPlugin::new()) .build()?;In an HTML form, emit the hidden input with {{ csrf_input }}, the {% csrf_token %} of umbral:
<form method="post" action="/contact"> {{ csrf_input }} <!-- fields... --></form>For JavaScript or htmx writes, send the raw token as a header instead. One attribute on <body> covers every htmx request on the page:
<body hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'>Seeing the token in your page source is expected
The CSRF token is not a secret. umbral uses double-submit: the same value lives in a non-HttpOnly cookie and in the page, and a write is accepted only when the two match. JavaScript has to be able to read it, which means anyone viewing source can too. What stops an attacker is the same-origin policy — a page on evil.example can neither read your cookie nor read the body of a response from your origin, so it cannot produce a matching pair. Tokens are also HMAC-signed with your app's secret_key by default (signed_csrf: true), so a cookie planted by a sibling subdomain can't forge a valid signature, and stale or unsigned cookies rotate automatically on the next safe request.
What would be a leak is a cache handing your rendered page — token, data and all — to somebody else. So any response to a personalised request (one carrying a session cookie, an Authorization, or a Proxy-Authorization header) ships Cache-Control: no-store, private. private bars shared caches like a CDN or a corporate proxy; no-store bars the browser's own disk and back/forward caches, which is what stops a logged-out user pressing Back into a rendered admin page.
Anonymous requests are left alone, so public pages stay CDN-cacheable, and a handler that sets its own Cache-Control always wins:
SecurityPlugin::with_config(SecurityConfig { // Only if a cache you control already keys on the session cookie. private_cache: false, ..Default::default()})Configuration is a struct, not a builder chain; flip exactly what you need:
SecurityPlugin::with_config(SecurityConfig { hsts: true, content_security_policy: Some("default-src 'self'".into()), csrf_exempt_paths: vec!["/api".into()], ..Default::default()})Install
cargo add umbral-securityProduction preset
The defaults are dev-safe: HSTS, CSP, and cross-origin isolation are off so local HTTP development works. A default deployment therefore ships with no XSS backstop (no CSP) and no SSL-stripping protection (no HSTS). Instead of hand-assembling those headers, use the one-call preset:
.plugin(SecurityPlugin::production_hardened())It turns on HSTS (with preload), a strict CSP baseline (default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'), Cross-Origin-Resource-Policy: same-origin, and marks the CSRF cookie Secure, while keeping every other secure default.