From 91cdaec6ac81f6f212c975909492036cefc3692a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Thu, 18 Jun 2026 03:26:21 +1200 Subject: [PATCH] feat(web): set baseline browser security headers on HTTP responses Adds a CSP, X-Frame-Options, X-Content-Type-Options and Permissions-Policy layer to every plain-HTTP response so the ZAP baseline scan passes. The CSP keeps the SPA working: inline styles for MUI/Emotion/xterm, data: images and fonts, and connect-src https: so WebTransport to the (different, configurable) WT origin is permitted. Triages the two non-actionable ZAP findings into .zap/rules.tsv: Suspicious Comments (false positive in the minified bundle) and Non-Storable Content (the SPA shell is no-store by design). --- .zap/rules.tsv | 2 + Cargo.lock | 1 + crates/web/Cargo.toml | 3 ++ crates/web/src/http.rs | 90 +++++++++++++++++++++++++++++++++++++++--- docs/spec/web.md | 10 +++++ 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/.zap/rules.tsv b/.zap/rules.tsv index 3cc2759a..5d103731 100644 --- a/.zap/rules.tsv +++ b/.zap/rules.tsv @@ -16,3 +16,5 @@ # proxy-owned or genuinely-not-applicable alerts, leaving real seedling-web # issues to fail the build. 10035 IGNORE (Strict-Transport-Security header — TLS is terminated and HSTS set by Caddy) +10027 IGNORE (Suspicious Comments — false positive matching tokens in the minified frontend bundle) +10049 IGNORE (Non-Storable Content — the SPA shell is served no-store by design, see spa.delivery) diff --git a/Cargo.lock b/Cargo.lock index 864bf0c1..1e6fac93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4666,6 +4666,7 @@ dependencies = [ "serde_json", "tokio", "toml", + "tower", "tower-http", "tracing", "uuid", diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index cae425bc..66d2250b 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -32,3 +32,6 @@ quinn.workspace = true rustls.workspace = true rust-embed.workspace = true wtransport.workspace = true + +[dev-dependencies] +tower = { version = "0.5.3", default-features = false, features = ["util"] } diff --git a/crates/web/src/http.rs b/crates/web/src/http.rs index a809216d..382f2284 100644 --- a/crates/web/src/http.rs +++ b/crates/web/src/http.rs @@ -1,21 +1,63 @@ use axum::extract::State; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header}; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Json, Router}; use serde_json::json; +use tower_http::set_header::SetResponseHeaderLayer; use crate::auth::{self, ConnectRequest}; use crate::spa; use crate::state::AppState; +// Restricts scripts and plugin content to same-origin and denies framing. +// `style-src` allows inline styles because the UI toolkit (MUI/Emotion, xterm) +// injects `