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 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-13 19:41:50 -07:00
parent 38eefb22e8
commit d60dc18add
2 changed files with 34 additions and 2 deletions
+1
View File
@@ -14,4 +14,5 @@ data/
# Android signing keystores — never commit
*.jks
*.jks.bak
*.jks.bak*
*.keystore
+33 -2
View File
@@ -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<axum::body::Body>, 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<serde_json::Value> {
axum::Json(serde_json::json!({