From 30c577d0c0185dc13a1f2f6a7bda88e741b5094a Mon Sep 17 00:00:00 2001 From: 81reap Date: Mon, 31 Aug 2026 15:47:31 -0400 Subject: [PATCH] fix(content_security_policy) :: send policies that have no nonce placeholder A `content_security_policy` without `'nonce-{NONCE}'` was silently ignored, leaving the response with no `Content-Security-Policy` header at all. (eg :: `default-src 'self'` gave less protection than configured. `configuration.md` alredy documents nonce-less policies as supported. --- CHANGELOG.md | 1 + src/webserver/content_security_policy.rs | 38 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce5788f..3ac7ab93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ``` SQLPage now keeps the variable value, producing `https://api.example.com/john.doe` as expected. +- A `content_security_policy` that does not contain `'nonce-{NONCE}'` is now sent as written, instead of being silently dropped and leaving the response with no `Content-Security-Policy` header at all. Setting the option to the empty string still disables the header, as documented. ## v0.46 diff --git a/src/webserver/content_security_policy.rs b/src/webserver/content_security_policy.rs index 32f9af72..72ccfdce 100644 --- a/src/webserver/content_security_policy.rs +++ b/src/webserver/content_security_policy.rs @@ -22,9 +22,11 @@ pub struct ContentSecurityPolicyTemplate { } impl ContentSecurityPolicyTemplate { + /// An empty template disables the header. Any other template is sent, + /// whether or not it contains the nonce placeholder. #[must_use] pub fn is_enabled(&self) -> bool { - self.nonce_position.is_some() + !self.template.is_empty() } fn format_nonce(&self, nonce: u64) -> String { @@ -87,6 +89,40 @@ impl ContentSecurityPolicy { #[cfg(test)] mod tests { use super::*; + use actix_web::http::StatusCode; + + fn header_for(template: &str) -> Option { + let mut response = HttpResponseBuilder::new(StatusCode::OK); + ContentSecurityPolicy::with_random_nonce().apply_to_response( + &ContentSecurityPolicyTemplate::from(template), + &mut response, + ); + response + .finish() + .headers() + .get(CONTENT_SECURITY_POLICY) + .map(|value| value.to_str().unwrap().to_owned()) + } + + #[test] + fn default_policy_substitutes_a_nonce() { + let header = header_for(DEFAULT_CONTENT_SECURITY_POLICY).unwrap(); + assert!(header.starts_with("script-src 'self' 'nonce-")); + assert!(!header.contains(NONCE_PLACEHOLDER)); + } + + #[test] + fn custom_policy_without_nonce_placeholder_is_sent_verbatim() { + assert_eq!( + header_for("default-src 'self'").as_deref(), + Some("default-src 'self'") + ); + } + + #[test] + fn empty_policy_disables_the_header() { + assert_eq!(header_for(""), None); + } #[test] fn test_content_security_policy_display() {