From 5791f082ebf2ae04d083e04916ed765a6ce16d51 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:46:31 +0000 Subject: [PATCH] Add security headers to backend responses Creates an Axum middleware function `security_headers` that applies recommended defense-in-depth headers like `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, and `Strict-Transport-Security` to all backend routes. Header names are passed as strictly lowercase to satisfy Axum's `HeaderMap::insert` constraint and prevent runtime panics. Additionally updates `.jules/sentinel.md` with relevant learnings about the lowercase header names constraint. Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ apps/server/src/main.rs | 2 ++ apps/server/src/security.rs | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1a95282559..35a1a425ff 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -17,3 +17,8 @@ **Vulnerability:** The `verify_github_sig` function in `apps/server/src/infra_routes.rs` verified GitHub webhook signatures using standard string comparison `expected.eq_ignore_ascii_case(hex_sig)`. **Learning:** String comparison functions exit early as soon as a character mismatch is found. In a cryptographic context, this variable response time leaks information and allows an attacker to perform a timing attack to forge a valid HMAC signature by guessing it byte by byte. **Prevention:** Always use a constant-time comparison library (like `hmac`'s `verify_slice()`) to compare cryptographic hashes, secrets, and signatures to prevent side-channel timing attacks. Avoid custom implementations of standard cryptographic algorithms. + +## 2026-11-06 - [Axum Security Headers & HeaderMap Constraints] +**Vulnerability:** The backend lacked common security headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Strict-Transport-Security`), leaving it slightly more exposed to MIME sniffing and clickjacking, despite its primary role being an API and agent workspace. +**Learning:** When using `HeaderMap::insert` with `HeaderValue::from_static` in Rust's `http` crate (which Axum uses), providing a string literal that contains uppercase letters (like `"X-Frame-Options"`) results in a type mismatch or runtime panic. The headers must be exclusively lower-case strings. +**Prevention:** Always use strictly lower-case strings (e.g., `"x-frame-options"`) when constructing standard header names manually in Axum middleware to ensure type safety and prevent runtime panics. diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index e4e00e95c3..90df19995a 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -217,6 +217,7 @@ async fn main() -> Result<(), Box> { }); } let mut app = worker::worker_router(state.clone()) + .layer(middleware::from_fn(security::security_headers)) .layer(middleware::from_fn(security::static_cache_headers)) .layer(security::cors_layer()) .layer(TraceLayer::new_for_http()); @@ -469,6 +470,7 @@ async fn main() -> Result<(), Box> { state.clone(), worker::fence_stale_leader, )) + .layer(middleware::from_fn(security::security_headers)) .layer(middleware::from_fn(security::static_cache_headers)) .layer(security::cors_layer()) .layer(TraceLayer::new_for_http()) diff --git a/apps/server/src/security.rs b/apps/server/src/security.rs index 8ea417ea1c..67b3f10918 100644 --- a/apps/server/src/security.rs +++ b/apps/server/src/security.rs @@ -69,3 +69,19 @@ pub async fn static_cache_headers(req: Request, next: Next) -> Response, next: Next) -> Response { + let mut res = next.run(req).await; + + let headers = res.headers_mut(); + + // Memory instructions: passing a string literal to HeaderMap::insert + // requires the string to be exclusively lowercase + headers.insert("x-frame-options", HeaderValue::from_static("DENY")); + headers.insert("x-content-type-options", HeaderValue::from_static("nosniff")); + headers.insert("referrer-policy", HeaderValue::from_static("strict-origin-when-cross-origin")); + headers.insert("strict-transport-security", HeaderValue::from_static("max-age=31536000; includeSubDomains")); + + res +}