diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1a9528255..674011f8f 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. + +## 2023-10-24 - [Add Security Headers to Backend] +**Vulnerability:** Missing critical security headers (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security) in Axum HTTP responses. +**Learning:** In axum, the `HeaderName::from_static` method strictly requires the header string to be lowercase (e.g., `x-frame-options`). Using uppercase letters will cause a runtime panic. The `security.rs` file manages static asset and cache headers via middleware where these security headers should logically reside. +**Prevention:** Always verify that all newly added HTTP headers in the axum backend use exclusively lowercase strings when passing to `from_static` or `insert`. Consider creating a unified security middleware in `security.rs` that applies strict defaults (like `nosniff` and HSTS) to all endpoints to enforce defense in depth. diff --git a/apps/server/src/security.rs b/apps/server/src/security.rs index 8ea417ea1..82dbeaff0 100644 --- a/apps/server/src/security.rs +++ b/apps/server/src/security.rs @@ -54,6 +54,18 @@ pub fn cors_layer() -> CorsLayer { pub async fn static_cache_headers(req: Request
, next: Next) -> Response { let path = req.uri().path().to_string(); let mut res = next.run(req).await; + + // Ajout des en-têtes de sécurité de base pour toutes les requêtes interceptées + if let Ok(v) = HeaderValue::from_str("DENY") { + res.headers_mut().insert(header::X_FRAME_OPTIONS, v); + } + if let Ok(v) = HeaderValue::from_str("nosniff") { + res.headers_mut().insert(header::X_CONTENT_TYPE_OPTIONS, v); + } + if let Ok(v) = HeaderValue::from_str("max-age=31536000; includeSubDomains") { + res.headers_mut().insert(header::STRICT_TRANSPORT_SECURITY, v); + } + let value = if path.starts_with("/_astro/") { "public, max-age=31536000, immutable" } else if path.ends_with(".html") @@ -67,5 +79,6 @@ pub async fn static_cache_headers(req: Request, next: Next) -> Response