Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions apps/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
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());
Expand Down Expand Up @@ -469,6 +470,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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())
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,19 @@ pub async fn static_cache_headers(req: Request<Body>, next: Next) -> Response<Bo
}
res
}

/// Security headers for all responses
pub async fn security_headers(req: Request<Body>, next: Next) -> Response<Body> {
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
}
Loading