From d60dc18addd97820a17c34d5fc6d2a7607b54c84 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 13 May 2026 19:41:50 -0700 Subject: [PATCH] fix(server): add CSP/security headers middleware, gitignore jks.bak* Content-Security-Policy, X-Content-Type-Options, and X-Frame-Options are now injected by a single Axum middleware on the web router subtree, so all HTML pages get consistent headers without touching each file. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + solitaire_server/src/lib.rs | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 100c1ed..a8dac76 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ data/ # Android signing keystores — never commit *.jks *.jks.bak +*.jks.bak* *.keystore diff --git a/solitaire_server/src/lib.rs b/solitaire_server/src/lib.rs index 36fe2f2..7102336 100644 --- a/solitaire_server/src/lib.rs +++ b/solitaire_server/src/lib.rs @@ -16,8 +16,9 @@ pub use auth::reset_password; use axum::{ extract::DefaultBodyLimit, + http::{HeaderValue, Request}, middleware as axum_middleware, - response::Html, + response::{Html, Response}, routing::{delete, get, post}, Router, }; @@ -226,7 +227,8 @@ fn build_router_inner(state: AppState, rate_limit: bool) -> Router { get(|| async { Html(include_str!("../web/replays.html")) }), ) .nest_service("/web", ServeDir::new("solitaire_server/web")) - .nest_service("/assets", ServeDir::new("assets")); + .nest_service("/assets", ServeDir::new("assets")) + .layer(axum_middleware::from_fn(security_headers)); Router::new() .merge(protected) @@ -238,6 +240,35 @@ fn build_router_inner(state: AppState, rate_limit: bool) -> Router { .with_state(state) } +const CSP: &str = concat!( + "default-src 'self'; ", + "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; ", + "style-src 'self' 'unsafe-inline'; ", + "font-src 'self'; ", + "img-src 'self' data:; ", + "connect-src 'self'; ", + "object-src 'none'; ", + "frame-ancestors 'none'", +); + +async fn security_headers(req: Request, next: axum_middleware::Next) -> Response { + let mut res = next.run(req).await; + let headers = res.headers_mut(); + headers.insert( + "Content-Security-Policy", + HeaderValue::from_static(CSP), + ); + headers.insert( + "X-Content-Type-Options", + HeaderValue::from_static("nosniff"), + ); + headers.insert( + "X-Frame-Options", + HeaderValue::from_static("DENY"), + ); + res +} + /// `GET /health` — simple liveness probe, no auth required. async fn health() -> axum::Json { axum::Json(serde_json::json!({