Skip to main content

Crate umbral_security

Crate umbral_security 

Source
Expand description

umbral-security — CSRF protection and a configurable security-header bundle.

CSRF protection plus a security-header bundle, widened to the modern header set. Plug it into the app and every non-safe request must carry a matching CSRF token; every response gets the hardening headers you’ve enabled.

App::builder()
    .plugin(AuthPlugin::new())
    .plugin(SecurityPlugin::new())   // secure-but-dev-safe defaults
    .build()
    .await?;

§Configuration is a struct, not a builder chain

Construct a SecurityConfig (every field has a secure, dev-safe default) and flip exactly what you need — no long .with_x().with_y() chain:

SecurityPlugin::with_config(SecurityConfig {
    hsts: true,
    content_security_policy: Some("default-src 'self'".into()),
    server_header: Some("umbral".into()),
    request_body_limit: Some(2 * 1024 * 1024),
    ..Default::default()
})

SecurityPlugin::new() keeps the defaults; SecurityPlugin::with_hsts(true) stays as a one-flag convenience.

§CSRF

Signed double-submit cookie pattern, fully automatic (see docs/decisions/2026-06-10-automatic-csrf.md):

  1. The middleware is the only mint. On GET / HEAD / OPTIONS it mints a token before the handler runs (first visit covered) and appends the umbral_csrf_token cookie to the response. The cookie is NOT HttpOnly: the page’s JS reads it and copies it into a header on later writes.
  2. Templates get the token for free. The token is scoped into umbral::templates::CURRENT_CSRF around every non-exempt request, so any rendered template can write {{ csrf_input }} (the full hidden input) or {{ csrf_token }} (raw value, for X-CSRF-Token headers / htmx hx-headers). View code never touches CSRF.
  3. Every POST / PUT / PATCH / DELETE must include the cookie AND a matching X-CSRF-Token header (JS path) or csrf_token / __csrf form field (HTML-form path). A mismatch returns 403. On success the token stays in scope so a validation-error re-render still carries it into the form.

The token is a 32-byte CSPRNG value, hex-encoded. The CSRF cookie gains Secure automatically under Environment::Prod (or force it with SecurityConfig::csrf_cookie_secure).

§Signed / session-bound CSRF (SecurityConfig::signed_csrf)

Naive double-submit trusts the cookie: an attacker who can plant a cookie on a sibling subdomain can forge a matching token. signed_csrf (default on) makes the token <random>.<HMAC-SHA256(secret_key, random[.session])> — a forged cookie can’t carry a valid signature without the app secret_key. Set SecurityConfig::session_bind_cookie to also fold the session cookie’s value into the signature so a token minted under one session can’t be replayed under another.

The flip to default-on is deploy-safe because the middleware rotates any cookie token that can’t pass signed-mode validation on the next safe request (browsers holding pre-upgrade unsigned cookies converge instead of 403ing), and because no other mint exists: the admin prefers the ambient middleware token and only self-mints when this plugin isn’t mounted. With no resolvable secret_key (tests, pre-App::build() renders) minting and validation degrade to plain double-submit instead of locking writes out. Opt back into plain double-submit with signed_csrf: false.

§Headers

Enabled by default: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-XSS-Protection: 0 (modern guidance disables the legacy auditor), Cross-Origin-Opener-Policy: same-origin, and a Server: umbral header. Opt-in (default off, each a field on SecurityConfig): Strict-Transport-Security, Content-Security-Policy, Permissions-Policy, Cross-Origin-Resource-Policy, Cross-Origin-Embedder-Policy. CSP and HSTS are off by default because a wrong value breaks apps (HSTS bricks http:// dev; a strict CSP breaks the CDN-using admin).

§Server identity & tower-http knobs

SecurityConfig::server_header sets the Server header (prefer a bare product name — a version is an information-disclosure tradeoff); SecurityConfig::hide_server_header strips whatever the stack added. SecurityConfig::request_body_limit caps the request body via tower-http’s RequestBodyLimitLayer (DoS hardening); SecurityConfig::redact_sensitive_headers (default on) marks authorization / cookie / set-cookie sensitive so they’re redacted in tracing output.

§Why this lives in Plugin::wrap_router

Layering middleware needs a tower::Layer value; the Plugin trait’s wrap_router(Router) -> Router lets each plugin layer its middleware with the full axum / tower API. The app builder calls it in topological order so security wraps everything declared before it.

Structs§

SecurityConfig
Declarative security configuration. Build from Default (secure, dev-safe) and override the fields you need — see the crate docs for the rationale behind each default.
SecurityPlugin
CSRF + security-headers plugin. Configure via SecurityConfig.

Functions§

current_csrf_token
Read the current CSRF token from the request’s cookie header. Public so handlers that render HTML forms can embed it as a hidden csrf_token input.
generate_token
Generate a fresh 32-byte token, hex-encoded. Public so tests and downstream code that mints tokens directly (e.g. server-rendered forms) share the same shape. Raw (unsigned) — the signed wrapper is applied by the middleware.
tokens_match
Constant-time string equality. Short-circuit == on String is a timing side-channel; ct_eq closes it. Per OWASP’s “Use Constant-Time String Comparison” rule for security tokens. Public so other token consumers (e.g. the admin’s SecurityPlugin-less login fallback) compare the same way.