rgoussu@goussu: ~/library/rust/rust-security
~/library/rust/rust-security cat web-security.md

Web security in Rust apps

# The OWASP concerns in Rust idiom — what memory safety does and doesn't buy, injection and XSS by construction, CSRF/CORS/headers via tower-http, sessions, panics as DoS, and serde-boundary hardening.

Conceptsaved 2026-08-09 #rust#security#web#owasp#middleware

Overview

Start with the honest ledger. What memory safety buys: buffer overflows, use-after-free, double-free and data races — the bug classes behind a large share of historical CVEs — cannot occur in safe Rust, and with them goes the classic RCE route. What it doesn't: everything above the memory model — injection, broken authz, SSRF, CSRF, misconfiguration, dependency compromise — exists here exactly as in Java and Go. Rust's honest differentiators on that upper layer are structural: parameterization and escaping are the default paths of its libraries, and the type system makes several mistake shapes unrepresentable. The new liability it adds: panics and resource exhaustion as the ecosystem's characteristic DoS surface.

Key points

  • Injection: sqlx/diesel are parameterized by construction — sqlx's macros cannot interpolate values into SQL text; string-built SQL requires visibly unusual code. Command injection: prefer std::process::Command arg arrays; never shell out with formatted strings.
  • XSS: the mainstream template engines auto-escape — askama (compile-time templates, type-checked variables), maud (markup as Rust macros), tera (runtime, Jinja-style) — with explicit |safe-style opt-outs as the audit points; API-first services mostly render JSON and inherit the browser-side story.
  • CSRF: SameSite=Lax/Strict cookies first (tower-sessions/axum-extra typed cookies); token-based via axum_csrf/actix equivalents when cross-site posts are real; bearer-token APIs are structurally immune, as everywhere.
  • CORS, headers, limits are tower-http layers: CorsLayer (explicit origins — resist the Any + credentials footgun; the types actually forbid that combo), SetResponseHeaderLayer for CSP/HSTS/nosniff (no helmet-bundle crate has become standard — keep an internal preset), RequestBodyLimitLayer + TimeoutLayer + tower::limit for request hygiene.
  • Sessions: tower-sessions with a server-side store (Redis, sqlx stores) — signed/encrypted cookie stores exist but server-side is the default posture; rotate on privilege change, same rules as everywhere.
  • Panics are the Rust-flavored DoS: an unwrap reachable from user input is a remote crash (and a task-poisoner under some architectures). Mitigations: clippy's unwrap_used lint on handler crates, catch_panic-style recovery layers at the boundary, and fuzzing the parsers — the testing side of this same coin.
  • serde-boundary hardening: deny_unknown_fields where strictness matters, bounded collection sizes (body limits do the heavy lifting), untagged-enum performance traps on hostile input, and #[serde(with)] validation at parse time — "parse, don't validate" is the idiom's native form here.
  • SSRF and path traversal: unchanged from the neighbours — allow-list outbound URL construction (the url crate for parsing, not string prefixes); tower-http's ServeDir handles traversal for static files.
  • What's not here: no memory-unsafe deserialization gadget chains (no runtime reflection — Java's deserialization nightmare has no Rust analog), and no unsafe-free path to corruption; the unsafe that does exist is auditable and testable.

Details

The composed stack, sketched

let app = Router::new()
    .merge(routes)
    .layer(TimeoutLayer::new(Duration::from_secs(10)))
    .layer(RequestBodyLimitLayer::new(1 << 20))          // 1 MiB
    .layer(cors_layer())                                 // explicit origins
    .layer(security_headers_layer())                     // CSP, HSTS, nosniff
    .layer(TraceLayer::new_for_http());

The same one-obvious-place property as Go's middleware stack — and the same silent failure mode if a layer is missing, which is why the internal-preset habit matters.

Related