mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(server): add HTTP security response headers middleware
Injects X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy, and Permissions-Policy on every response. Registered as outermost layer so it covers setup_guard 503s too. No new dependencies.
This commit is contained in:
parent
f9de282b0e
commit
3b3b0d7207
1 changed files with 92 additions and 0 deletions
|
|
@ -76,6 +76,42 @@ async fn setup_guard(
|
|||
next.run(req).await
|
||||
}
|
||||
|
||||
/// Tower middleware: injects HTTP security response headers on every response.
|
||||
/// HSTS is intentionally omitted — that belongs at the reverse-proxy layer.
|
||||
async fn security_headers(req: Request, next: Next) -> Response {
|
||||
let mut response = next.run(req).await;
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("x-content-type-options"),
|
||||
axum::http::HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("x-frame-options"),
|
||||
axum::http::HeaderValue::from_static("DENY"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("referrer-policy"),
|
||||
axum::http::HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("content-security-policy"),
|
||||
axum::http::HeaderValue::from_static(
|
||||
"default-src 'self'; \
|
||||
script-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline'; \
|
||||
img-src 'self' data: blob:; \
|
||||
font-src 'self'; \
|
||||
connect-src 'self'; \
|
||||
frame-ancestors 'none'",
|
||||
),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("permissions-policy"),
|
||||
axum::http::HeaderValue::from_static("camera=(), microphone=(), geolocation=()"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router {
|
||||
let state = AppState {
|
||||
registry: Arc::new(registry),
|
||||
|
|
@ -148,6 +184,7 @@ pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router
|
|||
.nest_service("/assets", ServeDir::new(static_dir.join("assets")))
|
||||
.fallback_service(ServeFile::new(static_dir.join("index.html")))
|
||||
.layer(axum::middleware::from_fn_with_state(state.clone(), setup_guard))
|
||||
.layer(axum::middleware::from_fn(security_headers))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
|
@ -2093,4 +2130,59 @@ mod tests {
|
|||
).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn security_headers_present_on_success_response() {
|
||||
let (test_app, _dir) = make_test_app();
|
||||
let response = test_app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/health")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.headers().get("x-content-type-options").unwrap(),
|
||||
"nosniff"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("x-frame-options").unwrap(),
|
||||
"DENY"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("referrer-policy").unwrap(),
|
||||
"strict-origin-when-cross-origin"
|
||||
);
|
||||
assert!(
|
||||
response.headers().get("content-security-policy").is_some(),
|
||||
"content-security-policy header must be present"
|
||||
);
|
||||
assert!(
|
||||
response.headers().get("permissions-policy").is_some(),
|
||||
"permissions-policy header must be present"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn security_headers_present_on_error_response() {
|
||||
let (test_app, _dir) = make_test_app();
|
||||
let response = test_app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/archives/nosucharchive/entries")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_ne!(response.status(), StatusCode::OK);
|
||||
assert!(response.headers().get("x-content-type-options").is_some());
|
||||
assert!(response.headers().get("x-frame-options").is_some());
|
||||
assert!(response.headers().get("referrer-policy").is_some());
|
||||
assert!(response.headers().get("content-security-policy").is_some());
|
||||
assert!(response.headers().get("permissions-policy").is_some());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue