Skip to main content

datasynth_server/rest/
security_headers.rs

1//! Security headers middleware.
2//!
3//! Injects security-related response headers on all responses.
4
5use axum::{
6    body::Body,
7    http::{header::HeaderValue, Request, Response},
8    middleware::Next,
9};
10
11/// Security headers middleware.
12///
13/// Adds the following headers to all responses:
14/// - `X-Content-Type-Options: nosniff`
15/// - `X-Frame-Options: DENY`
16/// - `X-XSS-Protection: 0` (modern best practice - rely on CSP instead)
17/// - `Referrer-Policy: strict-origin-when-cross-origin`
18/// - `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`
19/// - `Cache-Control: no-store` (API responses should not be cached)
20pub async fn security_headers_middleware(request: Request<Body>, next: Next) -> Response<Body> {
21    let mut response = next.run(request).await;
22    let headers = response.headers_mut();
23
24    headers.insert(
25        "x-content-type-options",
26        HeaderValue::from_static("nosniff"),
27    );
28    headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
29    headers.insert("x-xss-protection", HeaderValue::from_static("0"));
30    headers.insert(
31        "referrer-policy",
32        HeaderValue::from_static("strict-origin-when-cross-origin"),
33    );
34    headers.insert(
35        "content-security-policy",
36        HeaderValue::from_static("default-src 'none'; frame-ancestors 'none'"),
37    );
38    headers.insert("cache-control", HeaderValue::from_static("no-store"));
39
40    response
41}
42
43#[cfg(test)]
44#[allow(clippy::unwrap_used)]
45mod tests {
46    use super::*;
47    use axum::{routing::get, Router};
48    use tower::ServiceExt;
49
50    async fn ok_handler() -> &'static str {
51        "ok"
52    }
53
54    fn test_router() -> Router {
55        Router::new()
56            .route("/test", get(ok_handler))
57            .layer(axum::middleware::from_fn(security_headers_middleware))
58    }
59
60    #[tokio::test]
61    async fn test_security_headers_present() {
62        let router = test_router();
63        let request = Request::builder().uri("/test").body(Body::empty()).unwrap();
64
65        let response = router.oneshot(request).await.unwrap();
66        let headers = response.headers();
67
68        assert_eq!(headers.get("x-content-type-options").unwrap(), "nosniff");
69        assert_eq!(headers.get("x-frame-options").unwrap(), "DENY");
70        assert_eq!(headers.get("x-xss-protection").unwrap(), "0");
71        assert_eq!(
72            headers.get("referrer-policy").unwrap(),
73            "strict-origin-when-cross-origin"
74        );
75        assert_eq!(
76            headers.get("content-security-policy").unwrap(),
77            "default-src 'none'; frame-ancestors 'none'"
78        );
79        assert_eq!(headers.get("cache-control").unwrap(), "no-store");
80    }
81}