diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs index 6d99c0557..b2341b634 100644 --- a/moli-renderer-v8/src/document_runtime.rs +++ b/moli-renderer-v8/src/document_runtime.rs @@ -672,6 +672,7 @@ pub(crate) struct DocumentPolicyContainer { pub(crate) document_content_security_policies: Vec, pub(crate) response_content_security_policies: Vec, pub(crate) response_content_security_report_only_policies: Vec, + pub(crate) inherited_meta_content_security_policies: Vec, pub(crate) content_security_reporting_endpoints: crate::content_security_policy::ContentSecurityPolicyReportingEndpoints, pub(crate) credentialless: bool, diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index 214693b03..2a597d310 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -45,32 +45,32 @@ impl DocumentSubresourceCspKind { #[must_use = "report-only and enforced CSP results must be handled together"] pub(crate) struct DocumentContentSecurityPolicyCheck { - report_only_violation: Option, - enforced_violation: Option, + report_only_violations: Vec, + enforced_violations: Vec, } impl DocumentContentSecurityPolicyCheck { pub(crate) fn has_no_violations(&self) -> bool { - self.report_only_violation.is_none() && self.enforced_violation.is_none() + self.report_only_violations.is_empty() && self.enforced_violations.is_empty() } pub(crate) fn into_violations( self, ) -> ( - Option, - Option, + Vec, + Vec, ) { - (self.report_only_violation, self.enforced_violation) + (self.report_only_violations, self.enforced_violations) } #[cfg(test)] fn report_only_violation(&self) -> Option<&DocumentContentSecurityPolicyViolation> { - self.report_only_violation.as_ref() + self.report_only_violations.first() } #[cfg(test)] fn enforced_violation(&self) -> Option<&DocumentContentSecurityPolicyViolation> { - self.enforced_violation.as_ref() + self.enforced_violations.first() } } @@ -178,21 +178,21 @@ impl DocumentPolicyContainer { ) -> DocumentContentSecurityPolicyCheck { let DocumentNavigationEmbeddingContext::Nested(ancestor_origins) = embedding_context else { return DocumentContentSecurityPolicyCheck { - report_only_violation: None, - enforced_violation: None, + report_only_violations: Vec::new(), + enforced_violations: Vec::new(), }; }; DocumentContentSecurityPolicyCheck { - report_only_violation: - content_security_policy_frame_ancestors_violation_with_disposition_and_reporting_endpoints( + report_only_violations: + content_security_policy_frame_ancestors_violations_with_disposition_and_reporting_endpoints( &self.response_content_security_report_only_policies, protected_url, ancestor_origins, ContentSecurityPolicyDisposition::Report, &self.content_security_reporting_endpoints, ), - enforced_violation: - content_security_policy_frame_ancestors_violation_with_disposition_and_reporting_endpoints( + enforced_violations: + content_security_policy_frame_ancestors_violations_with_disposition_and_reporting_endpoints( &self.response_content_security_policies, protected_url, ancestor_origins, @@ -206,6 +206,7 @@ impl DocumentPolicyContainer { self.document_content_security_policies.clear(); self.response_content_security_policies.clear(); self.response_content_security_report_only_policies.clear(); + self.inherited_meta_content_security_policies.clear(); self.content_security_reporting_endpoints = Default::default(); self.sandbox = DocumentSandboxPolicy::default(); } @@ -352,7 +353,7 @@ impl DocumentRuntime { ); return; }; - let (report_only_violation, enforced_violation) = self + let (report_only_violations, enforced_violations) = self .inline_source_csp_check( ContentSecurityPolicyNonUrlKind::DocumentInlineStyleAttribute, source, @@ -360,13 +361,13 @@ impl DocumentRuntime { .into_violations(); unsafe { &mut *host_ptr }.set_element_inline_style_csp_state( target, - if enforced_violation.is_some() { + if !enforced_violations.is_empty() { crate::style_engine::InlineStyleCspState::BlockedAttribute } else { crate::style_engine::InlineStyleCspState::AllowedAttribute }, ); - for violation in [report_only_violation, enforced_violation] + for violation in [report_only_violations, enforced_violations] .into_iter() .flatten() { @@ -392,7 +393,7 @@ impl DocumentRuntime { let nonce = element.cryptographic_nonce().map(str::to_owned); let is_declarative_css_module = super::stylesheet_runtime::is_declarative_css_module_style_element(element); - let (style_report_only_violation, style_enforced_violation) = self + let (style_report_only_violations, style_enforced_violations) = self .inline_style_element_csp_check( &source, ContentSecurityPolicyStyleElementRequest { @@ -400,33 +401,33 @@ impl DocumentRuntime { }, ) .into_violations(); - let (script_report_only_violation, script_enforced_violation) = if is_declarative_css_module - { - // A declarative CSS module is not a script element and carries no - // creator-script trust to propagate through `strict-dynamic`. - // Use the parser-inserted request shape even when script created. - self.inline_script_element_csp_check( - &source, - ContentSecurityPolicyScriptElementRequest::parser_inserted_with_nonce( - nonce.as_deref(), - ), - ) - .into_violations() - } else { - (None, None) - }; + let (script_report_only_violations, script_enforced_violations) = + if is_declarative_css_module { + // A declarative CSS module is not a script element and carries no + // creator-script trust to propagate through `strict-dynamic`. + // Use the parser-inserted request shape even when script created. + self.inline_script_element_csp_check( + &source, + ContentSecurityPolicyScriptElementRequest::parser_inserted_with_nonce( + nonce.as_deref(), + ), + ) + .into_violations() + } else { + (Vec::new(), Vec::new()) + }; self.set_stylesheet_owner_csp_disposition( host_ptr, owner, super::StylesheetOwnerCspDisposition::from_blocked( - style_enforced_violation.is_some() || script_enforced_violation.is_some(), + !style_enforced_violations.is_empty() || !script_enforced_violations.is_empty(), ), ); for violation in [ - style_report_only_violation, - style_enforced_violation, - script_report_only_violation, - script_enforced_violation, + style_report_only_violations, + style_enforced_violations, + script_report_only_violations, + script_enforced_violations, ] .into_iter() .flatten() @@ -471,7 +472,7 @@ impl DocumentRuntime { ); return; }; - let (report_only_violation, enforced_violation) = self + let (report_only_violations, enforced_violations) = self .style_element_request_csp_check( &request_url, ContentSecurityPolicyStyleElementRequest { @@ -482,9 +483,9 @@ impl DocumentRuntime { self.set_stylesheet_owner_csp_disposition( host_ptr, owner, - super::StylesheetOwnerCspDisposition::from_blocked(enforced_violation.is_some()), + super::StylesheetOwnerCspDisposition::from_blocked(!enforced_violations.is_empty()), ); - for violation in [report_only_violation, enforced_violation] + for violation in [report_only_violations, enforced_violations] .into_iter() .flatten() { @@ -529,10 +530,6 @@ impl DocumentRuntime { DocumentSandboxPolicy::from_response_content_security_policies(policies); } - pub(crate) fn response_content_security_policies(&self) -> &[String] { - &self.policy_container.response_content_security_policies - } - #[cfg(test)] pub(crate) fn set_response_content_security_report_only_policies( &mut self, @@ -583,12 +580,6 @@ impl DocumentRuntime { self.policy_container.sandbox } - pub(crate) fn response_content_security_report_only_policies(&self) -> &[String] { - &self - .policy_container - .response_content_security_report_only_policies - } - #[cfg(test)] pub(crate) fn set_content_security_reporting_endpoints( &mut self, @@ -597,12 +588,6 @@ impl DocumentRuntime { self.policy_container.content_security_reporting_endpoints = endpoints; } - pub(crate) fn content_security_reporting_endpoints( - &self, - ) -> &ContentSecurityPolicyReportingEndpoints { - &self.policy_container.content_security_reporting_endpoints - } - #[cfg(test)] pub(crate) fn script_element_request_csp_violation( &self, @@ -718,14 +703,14 @@ impl DocumentRuntime { &self.policy_container.response_content_security_policies, &self.policy_container.content_security_reporting_endpoints, ); - let enforced_violation = document_connect_policy_violation_from_document_policies( + let enforced_violations = document_connect_policy_violations_from_document_policies( policies, document_url, request_url, redirect_status, ContentSecurityPolicyDisposition::Enforce, ); - let report_only_violation = document_connect_policy_violation( + let report_only_violations = document_connect_policy_violations( &self .policy_container .response_content_security_report_only_policies, @@ -736,8 +721,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -768,7 +753,7 @@ impl DocumentRuntime { &policy_container.response_content_security_policies, &policy_container.content_security_reporting_endpoints, ); - let enforced_violation = document_url_policy_violation_from_document_policies( + let enforced_violations = document_url_policy_violations_from_document_policies( policies, document_url, request_url, @@ -776,7 +761,7 @@ impl DocumentRuntime { ContentSecurityPolicyRedirectStatus::NoRedirect, ContentSecurityPolicyDisposition::Enforce, ); - let report_only_violation = document_url_policy_violation( + let report_only_violations = document_url_policy_violations( &policy_container.response_content_security_report_only_policies, &policy_container.content_security_reporting_endpoints, document_url, @@ -786,8 +771,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -839,8 +824,8 @@ impl DocumentRuntime { &self.policy_container.response_content_security_policies, &self.policy_container.content_security_reporting_endpoints, ); - let enforced_violation = - document_inline_style_element_policy_violation_from_document_policies( + let enforced_violations = + document_inline_style_element_policy_violations_from_document_policies( enforced_policies, document_url, source, @@ -853,8 +838,8 @@ impl DocumentRuntime { .response_content_security_report_only_policies, &self.policy_container.content_security_reporting_endpoints, ); - let report_only_violation = - document_inline_style_element_policy_violation_from_document_policies( + let report_only_violations = + document_inline_style_element_policy_violations_from_document_policies( report_only_policies, document_url, source, @@ -862,8 +847,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -879,22 +864,23 @@ impl DocumentRuntime { &self.policy_container.response_content_security_policies, &self.policy_container.content_security_reporting_endpoints, ); - let enforced_violation = document_style_element_url_policy_violation_from_document_policies( - enforced_policies, - document_url, - request_url, - ContentSecurityPolicyRedirectStatus::NoRedirect, - ContentSecurityPolicyDisposition::Enforce, - request, - ); + let enforced_violations = + document_style_element_url_policy_violations_from_document_policies( + enforced_policies, + document_url, + request_url, + ContentSecurityPolicyRedirectStatus::NoRedirect, + ContentSecurityPolicyDisposition::Enforce, + request, + ); let report_only_policies = document_response_content_security_policy_strings( &self .policy_container .response_content_security_report_only_policies, &self.policy_container.content_security_reporting_endpoints, ); - let report_only_violation = - document_style_element_url_policy_violation_from_document_policies( + let report_only_violations = + document_style_element_url_policy_violations_from_document_policies( report_only_policies, document_url, request_url, @@ -903,8 +889,8 @@ impl DocumentRuntime { request, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -918,22 +904,23 @@ impl DocumentRuntime { &self.policy_container.response_content_security_policies, &self.policy_container.content_security_reporting_endpoints, ); - let enforced_violation = document_style_element_url_policy_violation_from_document_policies( - enforced_policies, - document_url, - request_url, - ContentSecurityPolicyRedirectStatus::NoRedirect, - ContentSecurityPolicyDisposition::Enforce, - request, - ); + let enforced_violations = + document_style_element_url_policy_violations_from_document_policies( + enforced_policies, + document_url, + request_url, + ContentSecurityPolicyRedirectStatus::NoRedirect, + ContentSecurityPolicyDisposition::Enforce, + request, + ); let report_only_policies = document_response_content_security_policy_strings( &self .policy_container .response_content_security_report_only_policies, &self.policy_container.content_security_reporting_endpoints, ); - let report_only_violation = - document_style_element_url_policy_violation_from_document_policies( + let report_only_violations = + document_style_element_url_policy_violations_from_document_policies( report_only_policies, document_url, request_url, @@ -942,8 +929,8 @@ impl DocumentRuntime { request, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -964,8 +951,8 @@ impl DocumentRuntime { response_policies, response_reporting_endpoints, ); - let enforced_violation = - document_inline_script_element_policy_violation_from_document_policies( + let enforced_violations = + document_inline_script_element_policy_violations_from_document_policies( enforced_policies, document_url, source, @@ -976,8 +963,8 @@ impl DocumentRuntime { response_report_only_policies, response_reporting_endpoints, ); - let report_only_violation = - document_inline_script_element_policy_violation_from_document_policies( + let report_only_violations = + document_inline_script_element_policy_violations_from_document_policies( report_only_policies, document_url, source, @@ -985,8 +972,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -1006,7 +993,7 @@ impl DocumentRuntime { response_policies, response_reporting_endpoints, ); - let enforced_violation = document_inline_source_policy_violation_from_document_policies( + let enforced_violations = document_inline_source_policy_violations_from_document_policies( enforced_policies, document_url, kind, @@ -1017,16 +1004,17 @@ impl DocumentRuntime { response_report_only_policies, response_reporting_endpoints, ); - let report_only_violation = document_inline_source_policy_violation_from_document_policies( - report_only_policies, - document_url, - kind, - source, - ContentSecurityPolicyDisposition::Report, - ); + let report_only_violations = + document_inline_source_policy_violations_from_document_policies( + report_only_policies, + document_url, + kind, + source, + ContentSecurityPolicyDisposition::Report, + ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -1046,7 +1034,7 @@ impl DocumentRuntime { response_policies, response_reporting_endpoints, ); - let enforced_violation = document_non_url_policy_violation_from_document_policies( + let enforced_violations = document_non_url_policy_violations_from_document_policies( enforced_policies, document_url, kind, @@ -1057,7 +1045,7 @@ impl DocumentRuntime { response_report_only_policies, response_reporting_endpoints, ); - let report_only_violation = document_non_url_policy_violation_from_document_policies( + let report_only_violations = document_non_url_policy_violations_from_document_policies( report_only_policies, document_url, kind, @@ -1065,8 +1053,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -1187,14 +1175,14 @@ impl DocumentRuntime { response_policies, response_reporting_endpoints, ); - let enforced_violation = document_connect_policy_violation_from_document_policies( + let enforced_violations = document_connect_policy_violations_from_document_policies( policies, document_url, request_url, redirect_status, ContentSecurityPolicyDisposition::Enforce, ); - let report_only_violation = document_connect_policy_violation( + let report_only_violations = document_connect_policy_violations( response_report_only_policies, response_reporting_endpoints, document_url, @@ -1203,8 +1191,8 @@ impl DocumentRuntime { ContentSecurityPolicyDisposition::Report, ); DocumentContentSecurityPolicyCheck { - report_only_violation, - enforced_violation, + report_only_violations, + enforced_violations, } } @@ -1707,6 +1695,26 @@ impl DocumentRuntime { .unwrap_or_default() } + pub(crate) fn initialize_inherited_meta_content_security_policies( + &self, + document_handle: DomHandle, + policies: &[String], + ) { + if self.bypass_content_security_policy { + return; + } + // A new Document receives the creator's delivered meta policies once, + // before processing its own markup. They retain meta reporting rules. + let previous = self + .delivered_meta_content_security_policies + .borrow_mut() + .insert(document_handle, policies.to_vec()); + debug_assert!( + previous.is_none(), + "new Document already has delivered meta policies" + ); + } + pub(crate) fn process_parser_meta_content_security_policy(&self, handle: DomHandle) { self.process_meta_content_security_policy_handle(self.document_handle(), handle); } @@ -1759,6 +1767,20 @@ impl DocumentRuntime { } } +fn content_security_policy_frame_ancestors_violations_with_disposition_and_reporting_endpoints( + policies: &[String], + protected_url: &Url, + ancestor_origins: &[Option], + disposition: ContentSecurityPolicyDisposition, + reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, +) -> Vec { + policies.iter().filter_map(|policy| { + content_security_policy_frame_ancestors_violation_with_disposition_and_reporting_endpoints( + std::slice::from_ref(policy), protected_url, ancestor_origins, disposition, reporting_endpoints, + ) + }).collect() +} + fn document_connect_policy_violation( policies: &[String], reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, @@ -1778,6 +1800,25 @@ fn document_connect_policy_violation( ) } +fn document_connect_policy_violations( + policies: &[String], + reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, + document_url: &Url, + request_url: &Url, + redirect_status: ContentSecurityPolicyRedirectStatus, + disposition: ContentSecurityPolicyDisposition, +) -> Vec { + document_url_policy_violations( + policies, + reporting_endpoints, + document_url, + request_url, + ContentSecurityPolicyResourceKind::DocumentConnect, + redirect_status, + disposition, + ) +} + fn document_frame_policy_violation( policies: &[String], reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, @@ -1817,6 +1858,31 @@ fn document_url_policy_violation( ) } +fn document_url_policy_violations( + policies: &[String], + reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, + document_url: &Url, + request_url: &Url, + kind: ContentSecurityPolicyResourceKind, + redirect_status: ContentSecurityPolicyRedirectStatus, + disposition: ContentSecurityPolicyDisposition, +) -> Vec { + policies + .iter() + .filter_map(|policy| { + document_url_policy_violation( + std::slice::from_ref(policy), + reporting_endpoints, + document_url, + request_url, + kind, + redirect_status, + disposition, + ) + }) + .collect() +} + fn document_script_element_url_policy_violation( policies: &[String], reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, @@ -1878,14 +1944,14 @@ fn document_response_content_security_policy_strings( .collect() } -fn document_connect_policy_violation_from_document_policies( +fn document_connect_policy_violations_from_document_policies( policies: Vec, document_url: &Url, request_url: &Url, redirect_status: ContentSecurityPolicyRedirectStatus, disposition: ContentSecurityPolicyDisposition, -) -> Option { - document_url_policy_violation_from_document_policies( +) -> Vec { + document_url_policy_violations_from_document_policies( policies, document_url, request_url, @@ -1936,6 +2002,33 @@ fn document_url_policy_violation_from_document_policies( }) } +fn document_url_policy_violations_from_document_policies( + policies: Vec, + document_url: &Url, + request_url: &Url, + kind: ContentSecurityPolicyResourceKind, + redirect_status: ContentSecurityPolicyRedirectStatus, + disposition: ContentSecurityPolicyDisposition, +) -> Vec { + policies + .into_iter() + .filter_map(|policy| { + let single_policy = [policy.policy.clone()]; + let mut violation = document_url_policy_violation( + &single_policy, + &policy.reporting_endpoints, + document_url, + request_url, + kind, + redirect_status, + disposition, + )?; + apply_document_policy_reporting_flags(&mut violation, &policy); + Some(violation) + }) + .collect() +} + fn document_script_element_url_policy_violation_from_document_policies( policies: Vec, document_url: &Url, @@ -1960,28 +2053,31 @@ fn document_script_element_url_policy_violation_from_document_policies( }) } -fn document_style_element_url_policy_violation_from_document_policies( +fn document_style_element_url_policy_violations_from_document_policies( policies: Vec, document_url: &Url, request_url: &Url, redirect_status: ContentSecurityPolicyRedirectStatus, disposition: ContentSecurityPolicyDisposition, request: ContentSecurityPolicyStyleElementRequest<'_>, -) -> Option { - policies.into_iter().find_map(|policy| { - let single_policy = [policy.policy.clone()]; - let mut violation = document_style_element_url_policy_violation( - &single_policy, - &policy.reporting_endpoints, - document_url, - request_url, - redirect_status, - disposition, - request, - )?; - apply_document_policy_reporting_flags(&mut violation, &policy); - Some(violation) - }) +) -> Vec { + policies + .into_iter() + .filter_map(|policy| { + let single_policy = [policy.policy.clone()]; + let mut violation = document_style_element_url_policy_violation( + &single_policy, + &policy.reporting_endpoints, + document_url, + request_url, + redirect_status, + disposition, + request, + )?; + apply_document_policy_reporting_flags(&mut violation, &policy); + Some(violation) + }) + .collect() } fn document_non_url_policy_violation_from_document_policies( @@ -2005,14 +2101,38 @@ fn document_non_url_policy_violation_from_document_policies( }) } -fn document_inline_source_policy_violation_from_document_policies( +fn document_non_url_policy_violations_from_document_policies( + policies: Vec, + document_url: &Url, + kind: ContentSecurityPolicyNonUrlKind, + source: Option<&str>, + disposition: ContentSecurityPolicyDisposition, +) -> Vec { + policies + .into_iter() + .filter_map(|policy| { + let mut violation = content_security_policy_non_url_violation_with_source( + &policy.policy, + document_url, + kind, + source, + disposition, + &policy.reporting_endpoints, + )?; + apply_document_policy_reporting_flags(&mut violation, &policy); + Some(violation) + }) + .collect() +} + +fn document_inline_source_policy_violations_from_document_policies( policies: Vec, document_url: &Url, kind: ContentSecurityPolicyNonUrlKind, source: &str, disposition: ContentSecurityPolicyDisposition, -) -> Option { - policies.into_iter().find_map(|policy| { +) -> Vec { + policies.into_iter().filter_map(|policy| { let mut violation = content_security_policy_inline_source_violation_with_disposition_and_reporting_endpoints( &policy.policy, document_url, @@ -2023,17 +2143,17 @@ fn document_inline_source_policy_violation_from_document_policies( )?; apply_document_policy_reporting_flags(&mut violation, &policy); Some(violation) - }) + }).collect() } -fn document_inline_script_element_policy_violation_from_document_policies( +fn document_inline_script_element_policy_violations_from_document_policies( policies: Vec, document_url: &Url, source: &str, request: ContentSecurityPolicyScriptElementRequest<'_>, disposition: ContentSecurityPolicyDisposition, -) -> Option { - policies.into_iter().find_map(|policy| { +) -> Vec { + policies.into_iter().filter_map(|policy| { let mut violation = content_security_policy_inline_script_element_violation_with_disposition_and_reporting_endpoints( &policy.policy, document_url, @@ -2044,17 +2164,17 @@ fn document_inline_script_element_policy_violation_from_document_policies( )?; apply_document_policy_reporting_flags(&mut violation, &policy); Some(violation) - }) + }).collect() } -fn document_inline_style_element_policy_violation_from_document_policies( +fn document_inline_style_element_policy_violations_from_document_policies( policies: Vec, document_url: &Url, source: &str, request: ContentSecurityPolicyStyleElementRequest<'_>, disposition: ContentSecurityPolicyDisposition, -) -> Option { - policies.into_iter().find_map(|policy| { +) -> Vec { + policies.into_iter().filter_map(|policy| { let mut violation = content_security_policy_inline_style_element_violation_with_disposition_and_reporting_endpoints( &policy.policy, document_url, @@ -2065,7 +2185,7 @@ fn document_inline_style_element_policy_violation_from_document_policies( )?; apply_document_policy_reporting_flags(&mut violation, &policy); Some(violation) - }) + }).collect() } #[cfg(test)] @@ -2236,6 +2356,206 @@ mod tests { runtime } + #[test] + fn document_csp_checks_report_each_violated_header_and_meta_policy() { + let mut runtime = runtime_for_html( + r#""#, + ); + let enforced = [ + "default-src 'none'; report-uri /enforce-first", + "base-uri 'none'", + "default-src https://other.test; report-uri /enforce-second", + ] + .map(str::to_owned); + let report_only = [ + "default-src 'none'; report-uri /report-first", + "base-uri 'none'", + "default-src https://other.test; report-uri /report-second", + ] + .map(str::to_owned); + runtime.set_response_content_security_policies(&enforced); + runtime.set_response_content_security_report_only_policies(&report_only); + let request_url = Url::parse("https://example.test/resource").unwrap(); + let mut checks = Vec::from( + [ + DocumentSubresourceCspKind::Image, + DocumentSubresourceCspKind::Manifest, + DocumentSubresourceCspKind::Media, + ] + .map(|kind| runtime.document_subresource_csp_check(&request_url, kind)), + ); + checks.push(runtime.document_connect_csp_check_with_redirect_status( + &request_url, + ContentSecurityPolicyRedirectStatus::FollowedRedirect, + )); + checks.push(runtime.style_element_request_csp_check( + &request_url, + ContentSecurityPolicyStyleElementRequest { nonce: None }, + )); + checks.push(runtime.inline_style_element_csp_check( + "body { color: red; }", + ContentSecurityPolicyStyleElementRequest { nonce: None }, + )); + checks.push(runtime.inline_script_element_csp_check( + "globalThis.ran = true", + ContentSecurityPolicyScriptElementRequest::parser_inserted_with_nonce(None), + )); + for kind in [ + ContentSecurityPolicyNonUrlKind::DocumentInlineEventHandler, + ContentSecurityPolicyNonUrlKind::DocumentInlineStyleAttribute, + ] { + checks.push(runtime.inline_source_csp_check(kind, "blocked inline source")); + } + for kind in [ + ContentSecurityPolicyNonUrlKind::Eval, + ContentSecurityPolicyNonUrlKind::WasmEval, + ] { + checks.push(runtime.non_url_csp_check_for_document( + Some(runtime.document_handle()), + runtime.document_url(), + &enforced, + &report_only, + &Default::default(), + kind, + Some("blocked eval source"), + )); + } + for check in checks { + let (reported, blocked) = check.into_violations(); + assert_eq!(reported.len(), 2); + assert_eq!(blocked.len(), 3); + for (violations, policies, disposition, endpoints) in [ + ( + &reported, + &report_only, + ContentSecurityPolicyDisposition::Report, + [ + "https://example.test/report-first", + "https://example.test/report-second", + ], + ), + ( + &blocked, + &enforced, + ContentSecurityPolicyDisposition::Enforce, + [ + "https://example.test/enforce-first", + "https://example.test/enforce-second", + ], + ), + ] { + for (index, policy_index) in [0, 2].into_iter().enumerate() { + let violation = &violations[index]; + assert_eq!(violation.original_policy, policies[policy_index]); + assert_eq!(violation.disposition, disposition); + assert_eq!(violation.document_uri, runtime.document_url().as_str()); + assert_eq!(violation.report_uri_endpoints, vec![endpoints[index]]); + } + } + assert_eq!( + blocked[2].original_policy, + "default-src 'none'; report-uri /ignored-meta" + ); + assert!(blocked[2].report_uri_endpoints.is_empty()); + } + } + + #[test] + fn document_csp_checks_preserve_identical_policies_from_separate_deliveries() { + let mut runtime = runtime_for_html( + r#""#, + ); + let policies = ["img-src 'none'".to_owned(), "img-src 'none'".to_owned()]; + runtime.set_response_content_security_policies(&policies); + runtime.set_response_content_security_report_only_policies(&policies); + let (reported, blocked) = runtime + .document_subresource_csp_check( + &Url::parse("https://example.test/image").unwrap(), + DocumentSubresourceCspKind::Image, + ) + .into_violations(); + assert_eq!(reported.len(), 2); + assert_eq!(blocked.len(), 3); + assert!( + reported + .iter() + .chain(&blocked) + .all(|violation| violation.original_policy == "img-src 'none'") + ); + } + + #[test] + fn inherited_meta_policies_preserve_reporting_rules_and_duplicate_deliveries() { + let mut runtime = runtime_for_html(""); + let policy = "img-src 'none'; report-uri /report".to_owned(); + runtime.initialize_inherited_meta_content_security_policies( + runtime.document_handle(), + &[policy.clone(), policy.clone()], + ); + runtime.set_response_content_security_policies(std::slice::from_ref(&policy)); + let (_, blocked) = runtime + .document_subresource_csp_check( + &Url::parse("https://example.test/image").unwrap(), + DocumentSubresourceCspKind::Image, + ) + .into_violations(); + assert_eq!(blocked.len(), 3); + assert!( + blocked + .iter() + .all(|violation| violation.original_policy == policy) + ); + assert_eq!( + blocked[0].report_uri_endpoints, + vec!["https://example.test/report"] + ); + assert!( + blocked[1..] + .iter() + .all(|violation| violation.report_uri_endpoints.is_empty()) + ); + } + + #[test] + fn frame_ancestors_reports_once_per_policy_even_when_multiple_ancestors_fail() { + let protected_url = Url::parse("https://child.test/frame").unwrap(); + let policy = DocumentPolicyContainer::from_navigation_response_headers( + &[ + ( + "Content-Security-Policy".to_owned(), + "frame-ancestors 'none', frame-ancestors 'self'".to_owned(), + ), + ( + "Content-Security-Policy-Report-Only".to_owned(), + "frame-ancestors 'none', frame-ancestors 'self'".to_owned(), + ), + ], + &protected_url, + ); + let ancestors = [Some(Url::parse("https://parent.test").unwrap()), None]; + let (reported, blocked) = policy + .navigation_response_frame_ancestors_check( + &protected_url, + DocumentNavigationEmbeddingContext::Nested(&ancestors), + ) + .into_violations(); + assert_eq!(reported.len(), 2); + assert_eq!(blocked.len(), 2); + for violations in [&reported, &blocked] { + assert_eq!(violations[0].original_policy, "frame-ancestors 'none'"); + assert_eq!(violations[1].original_policy, "frame-ancestors 'self'"); + } + assert!( + policy + .navigation_response_frame_ancestors_check( + &protected_url, + DocumentNavigationEmbeddingContext::TopLevel, + ) + .has_no_violations() + ); + } + fn runtime_with_response_csp_report_only(policy: &str) -> DocumentRuntime { let mut runtime = runtime_for_html(""); runtime.set_response_content_security_report_only_policies(&[policy.to_owned()]); @@ -3289,12 +3609,14 @@ mod tests { DocumentNavigationEmbeddingContext::Nested(&ancestors), ) .into_violations(); + assert_eq!(report_only.len(), 1); + assert_eq!(enforced.len(), 1); assert_eq!( - report_only.unwrap().disposition, + report_only[0].disposition, ContentSecurityPolicyDisposition::Report ); assert_eq!( - enforced.unwrap().disposition, + enforced[0].disposition, ContentSecurityPolicyDisposition::Enforce ); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs index d36cf8f0d..f9b36022e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs @@ -324,6 +324,10 @@ impl JsContextHost { expected_current_owner, )?; debug_assert_eq!(owner_transition.retired_owner(), expected_current_owner); + unsafe { &*self.runtime }.initialize_inherited_meta_content_security_policies( + document_handle, + &document_policy_container.inherited_meta_content_security_policies, + ); let ancestor_origins_refreshed = self.refresh_current_child_document_ancestor_origins(handle); debug_assert!( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs index c3d832025..cbb7430ec 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs @@ -41,10 +41,16 @@ impl JsContextHost { .dom_host() .node(handle) .and_then(crate::dom::native::Node::owner_document) - && let Some(permissions_policy) = - self.document_permissions_policy_for_document_handle(parent_document) { - policy_container.permissions_policy = permissions_policy; + if let Some(permissions_policy) = + self.document_permissions_policy_for_document_handle(parent_document) + { + policy_container.permissions_policy = permissions_policy; + } + // Inheritance clones the creator's policy list at navigation time. + // Later parent meta mutations must not alter the child's policies. + policy_container.inherited_meta_content_security_policies = unsafe { &*self.runtime } + .meta_content_security_policy_strings_for_document(parent_document); } policy_container.document_referrer = self.document_url_for_child_context(handle).to_string(); @@ -101,6 +107,12 @@ impl JsContextHost { let document_url = init.document_url.clone(); let document_handle = self.create_empty_live_child_html_document(document_url.clone(), Some("text/html")); + unsafe { &*self.runtime }.initialize_inherited_meta_content_security_policies( + document_handle, + &init + .policy_container + .inherited_meta_content_security_policies, + ); self.dom_host_mut() .set_document_fallback_base_url_for_handle( document_handle, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs index deb93501f..5c5734d35 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs @@ -419,16 +419,16 @@ impl JsContextHost { .clear_content_security_policy_for_bypass(); } let ancestor_origins = self.child_document_frame_ancestor_origins(handle); - let (report_only_violation, enforced_violation) = loaded + let (report_only_violations, enforced_violations) = loaded .policy_container .navigation_response_frame_ancestors_check( &loaded.final_url, DocumentNavigationEmbeddingContext::Nested(&ancestor_origins), ) .into_violations(); - for violation in report_only_violation + for violation in report_only_violations .iter() - .chain(enforced_violation.iter()) + .chain(enforced_violations.iter()) { // The protected response never receives a Document when enforcement blocks // it, so there is no target on which to dispatch a DOM event. Keep the @@ -446,7 +446,7 @@ impl JsContextHost { &violation.report_to_endpoints, ); } - if let Some(violation) = enforced_violation { + if let Some(violation) = enforced_violations.into_iter().next() { Err(format!( "child document response blocked by Content Security Policy `{}` for `{}`", violation.effective_directive, violation.blocked_uri diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs index 79cbdeee8..62d56fb90 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs @@ -28,9 +28,12 @@ impl JsContextHost { handle: DomHandle, url: &Url, ) -> Option { - let snapshot = self.apply_page_csp_bypass_to_child_snapshot( - self.materialize_local_child_snapshot_for_url(url)?, - ); + let mut snapshot = self.materialize_local_child_snapshot_for_url(url)?; + if ChildBrowsingContextBootstrap::Url(url.clone()).content_security_policy_inherited() { + snapshot.policy_container = + self.initial_child_about_blank_policy_container_from_parent(handle); + } + let snapshot = self.apply_page_csp_bypass_to_child_snapshot(snapshot); if moli_url::is_about_blank(&snapshot.url) { Some(snapshot.with_fallback_base_url(self.document_base_url_for_child_context(handle))) } else { @@ -151,31 +154,29 @@ impl JsContextHost { handle: DomHandle, bootstrap: &ChildBrowsingContextBootstrap, ) -> Option { - match bootstrap { + let mut snapshot = match bootstrap { ChildBrowsingContextBootstrap::AboutBlank => { - let policy_container = - self.initial_child_about_blank_policy_container_from_parent(handle); - Some( - self.apply_page_csp_bypass_to_child_snapshot( - ChildBrowsingContextSnapshot::about_blank( - self.document_base_url_for_child_context(handle), - ) - .with_policy_container(policy_container), - ), - ) + Some(ChildBrowsingContextSnapshot::about_blank( + self.document_base_url_for_child_context(handle), + )) } - ChildBrowsingContextBootstrap::Srcdoc { base_url, markup } => Some( - self.apply_page_csp_bypass_to_child_snapshot(ChildBrowsingContextSnapshot::srcdoc( + ChildBrowsingContextBootstrap::Srcdoc { base_url, markup } => { + Some(ChildBrowsingContextSnapshot::srcdoc( base_url.clone(), markup.clone(), self.document_character_set().to_owned(), - )), - ), + )) + } ChildBrowsingContextBootstrap::Url(url) => { - self.materialize_local_child_snapshot_for_navigation_url(handle, url) + return self.materialize_local_child_snapshot_for_navigation_url(handle, url); } ChildBrowsingContextBootstrap::Request(_) => None, + }?; + if bootstrap.content_security_policy_inherited() { + snapshot.policy_container = + self.initial_child_about_blank_policy_container_from_parent(handle); } + Some(self.apply_page_csp_bypass_to_child_snapshot(snapshot)) } pub(in crate::native_bridge::context_host) fn child_document_fallback_character_set( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index 24d0e9764..3b2fb99cf 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -230,10 +230,6 @@ impl ChildBrowsingContextEntry { self.live_bootstrap.security_origin_inherited() } - pub(super) fn content_security_policy_inherited(&self) -> bool { - self.live_bootstrap.content_security_policy_inherited() - } - pub(super) fn document_policy_container_snapshot(&self) -> ChildDocumentPolicyContainer { self.document_policy_container.clone() } @@ -338,10 +334,6 @@ impl ChildBrowsingContextEntry { .as_slice() } - pub(super) fn has_response_content_security_policies(&self) -> bool { - !self.response_content_security_policies().is_empty() - } - pub(super) fn content_security_reporting_endpoints( &self, ) -> crate::content_security_policy::ContentSecurityPolicyReportingEndpoints { @@ -367,6 +359,10 @@ impl ChildBrowsingContextEntry { self.document_policy_container .document_content_security_policies = policy_container.document_content_security_policies.clone(); + self.document_policy_container + .inherited_meta_content_security_policies = policy_container + .inherited_meta_content_security_policies + .clone(); self.document_policy_container .response_content_security_policies = policy_container.response_content_security_policies.clone(); @@ -405,6 +401,11 @@ impl ChildBrowsingContextEntry { snapshot.policy_container.document_isolation_policy; self.document_policy_container.cross_origin_isolated = snapshot.policy_container.cross_origin_isolated; + self.document_policy_container + .inherited_meta_content_security_policies = snapshot + .policy_container + .inherited_meta_content_security_policies + .clone(); self.document_policy_container .response_content_security_policies = snapshot .policy_container diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index 00bbf7642..8fae63767 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -363,6 +363,9 @@ impl JsContextHost { content_security_reporting_endpoints: refresh_policy_source .map(|policy| policy.content_security_reporting_endpoints.clone()) .unwrap_or_default(), + inherited_meta_content_security_policies: refresh_policy_source + .map(|policy| policy.inherited_meta_content_security_policies.clone()) + .unwrap_or_default(), permissions_policy: if is_new { // The synchronous initial about:blank Document is // already subject to the iframe's container policy. diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index 7ed4db05e..717821ad5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -508,6 +508,9 @@ impl LightweightPopupDocumentState { self.policy_container.cross_origin_isolated = response.cross_origin_isolated; self.policy_container.document_content_security_policies = response.document_content_security_policies; + self.policy_container + .inherited_meta_content_security_policies = + response.inherited_meta_content_security_policies; self.policy_container.response_content_security_policies = response.response_content_security_policies; self.policy_container diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index 6e6b2bbbf..4074f4c24 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -99,14 +99,8 @@ impl JsContextHost { }) } OwnerDispatchScope::Child(handle) => { - let mut policy_container = + let policy_container = self.child_browsing_context_policy_container_snapshot(handle)?; - policy_container.response_content_security_policies = - self.child_effective_enforced_content_security_policies(handle); - policy_container.response_content_security_report_only_policies = - self.child_effective_response_content_security_report_only_policies(handle); - policy_container.content_security_reporting_endpoints = - self.child_effective_content_security_reporting_endpoints(handle); Some(OwnerDocumentPolicySnapshot { document_handle: self.child_browsing_context_document_handle(handle), document_url: self.child_browsing_context_current_url(handle)?, @@ -300,22 +294,24 @@ impl JsContextHost { if policy_owner_dispatch_scope(scope) != OwnerDispatchScope::Top { return DocumentCspOutcome::SkippedNonTopContext; } - let (report_only_violation, enforced_violation) = self + let (report_only_violations, enforced_violations) = self .document_subresource_csp_check(request_url, kind) .into_violations(); let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_best_effort( scope, host_ptr, &violation, ); } - let Some(violation) = enforced_violation else { - return DocumentCspOutcome::Allowed; - }; - self.dispatch_content_security_policy_violation_event_best_effort( - scope, host_ptr, &violation, - ); - DocumentCspOutcome::Blocked(violation) + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_best_effort( + scope, host_ptr, violation, + ); + } + match enforced_violations.into_iter().next() { + Some(violation) => DocumentCspOutcome::Blocked(violation), + None => DocumentCspOutcome::Allowed, + } } pub(crate) fn check_element_subresource_csp<'s>( @@ -334,7 +330,7 @@ impl JsContextHost { // The element's Document owns this request even when a different // Window's script changes its source or queues the update microtask. // SAFETY: this host and its DocumentRuntime are owned by the same ScriptVm. - let (report_only_violation, enforced_violation) = unsafe { &*self.runtime } + let (report_only_violations, enforced_violations) = unsafe { &*self.runtime } .document_subresource_csp_check_for_document( snapshot.document_handle, &snapshot.document_url, @@ -344,18 +340,20 @@ impl JsContextHost { ) .into_violations(); let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_for_owner_best_effort( scope, host_ptr, owner, &violation, ); } - let Some(violation) = enforced_violation else { - return DocumentCspOutcome::Allowed; - }; - self.dispatch_content_security_policy_violation_event_for_owner_best_effort( - scope, host_ptr, owner, &violation, - ); - DocumentCspOutcome::Blocked(violation) + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_for_owner_best_effort( + scope, host_ptr, owner, violation, + ); + } + match enforced_violations.into_iter().next() { + Some(violation) => DocumentCspOutcome::Blocked(violation), + None => DocumentCspOutcome::Allowed, + } } pub(crate) fn owner_dispatch_scope_for_node( @@ -404,34 +402,33 @@ impl JsContextHost { else { return true; }; - let (mut report_only_violation, mut enforced_violation) = check.into_violations(); + let (mut report_only_violations, mut enforced_violations) = check.into_violations(); if owner == OwnerDispatchScope::Top && let Some(position) = unsafe { &*self.runtime }.parser_script_start_position(script) { let line = i32::try_from(position.line).unwrap_or(i32::MAX); let column = i32::try_from(position.column).unwrap_or(i32::MAX); - if let Some(violation) = report_only_violation.as_mut() { + for violation in &mut report_only_violations { violation.line_number = line; violation.column_number = column; } - if let Some(violation) = enforced_violation.as_mut() { + for violation in &mut enforced_violations { violation.line_number = line; violation.column_number = column; } } let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_for_element_owner_best_effort( scope, host_ptr, owner, script, &violation, ); } - let Some(violation) = enforced_violation else { - return true; - }; - self.dispatch_content_security_policy_violation_event_for_element_owner_best_effort( - scope, host_ptr, owner, script, &violation, - ); - false + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_for_element_owner_best_effort( + scope, host_ptr, owner, script, violation, + ); + } + enforced_violations.is_empty() } pub(crate) fn allows_inline_javascript_navigation_by_csp<'s>( @@ -463,20 +460,19 @@ impl JsContextHost { // owner. Committed documents must always produce a check. return true; }; - let (report_only_violation, enforced_violation) = check.into_violations(); + let (report_only_violations, enforced_violations) = check.into_violations(); let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_for_owner_best_effort( scope, host_ptr, owner, &violation, ); } - let Some(violation) = enforced_violation else { - return true; - }; - self.dispatch_content_security_policy_violation_event_for_owner_best_effort( - scope, host_ptr, owner, &violation, - ); - false + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_for_owner_best_effort( + scope, host_ptr, owner, violation, + ); + } + enforced_violations.is_empty() } fn inline_source_csp_check_for_owner( @@ -574,20 +570,22 @@ impl JsContextHost { ) else { return DocumentCspOutcome::Allowed; }; - let (report_only_violation, enforced_violation) = check.into_violations(); + let (report_only_violations, enforced_violations) = check.into_violations(); let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_for_owner_best_effort( scope, host_ptr, owner, &violation, ); } - let Some(violation) = enforced_violation else { - return DocumentCspOutcome::Allowed; - }; - self.dispatch_content_security_policy_violation_event_for_owner_best_effort( - scope, host_ptr, owner, &violation, - ); - DocumentCspOutcome::Blocked(violation) + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_for_owner_best_effort( + scope, host_ptr, owner, violation, + ); + } + match enforced_violations.into_iter().next() { + Some(violation) => DocumentCspOutcome::Blocked(violation), + None => DocumentCspOutcome::Allowed, + } } fn document_connect_csp_check_for_owner_with_redirect_status( @@ -637,7 +635,7 @@ impl JsContextHost { request_url, ContentSecurityPolicyRedirectStatus::NoRedirect, ) - .map(|check| check.into_violations().1.is_none()) + .map(|check| check.into_violations().1.is_empty()) .unwrap_or(true) } @@ -726,133 +724,16 @@ impl JsContextHost { ) } - fn child_effective_enforced_content_security_policies( - &self, - child_handle: DomHandle, - ) -> Vec { - let mut policies = Vec::new(); - if self - .child_browsing_contexts - .get(&child_handle) - .is_some_and(|entry| entry.content_security_policy_inherited()) - { - policies.extend(self.parent_effective_enforced_content_security_policies(child_handle)); - } - policies.extend( - self.child_browsing_contexts - .get(&child_handle) - .map(|entry| entry.response_content_security_policies().to_vec()) - .unwrap_or_default(), - ); - policies - } - - fn child_effective_response_content_security_report_only_policies( - &self, - child_handle: DomHandle, - ) -> Vec { - let mut policies = Vec::new(); - if self - .child_browsing_contexts - .get(&child_handle) - .is_some_and(|entry| entry.content_security_policy_inherited()) - { - policies.extend( - self.parent_effective_response_content_security_report_only_policies(child_handle), - ); - } - policies.extend( - self.child_browsing_contexts - .get(&child_handle) - .map(|entry| { - entry - .response_content_security_report_only_policies() - .to_vec() - }) - .unwrap_or_default(), - ); - policies - } - - fn parent_effective_enforced_content_security_policies( - &self, - child_handle: DomHandle, - ) -> Vec { - match self.child_browsing_context_parent_handle(child_handle) { - Some(parent) => { - let mut policies = self.child_effective_enforced_content_security_policies(parent); - if let Some(document) = self.child_browsing_context_document_handle(parent) { - // SAFETY: JsContextHost is owned by the ScriptVm that owns this - // DocumentRuntime. - policies.extend( - unsafe { &*self.runtime } - .meta_content_security_policy_strings_for_document(document), - ); - } - policies - } - None => { - // SAFETY: JsContextHost is owned by the ScriptVm that owns this - // DocumentRuntime. - let runtime = unsafe { &*self.runtime }; - let mut policies = runtime.response_content_security_policies().to_vec(); - policies.extend( - runtime.meta_content_security_policy_strings_for_document( - runtime.document_handle(), - ), - ); - policies - } - } - } - - fn parent_effective_response_content_security_report_only_policies( - &self, - child_handle: DomHandle, - ) -> Vec { - match self.child_browsing_context_parent_handle(child_handle) { - Some(parent) => { - self.child_effective_response_content_security_report_only_policies(parent) - } - None => unsafe { &*self.runtime } - .response_content_security_report_only_policies() - .to_vec(), - } - } - fn child_effective_content_security_reporting_endpoints( &self, child_handle: DomHandle, ) -> ContentSecurityPolicyReportingEndpoints { - let inherits_parent = self - .child_browsing_contexts - .get(&child_handle) - .is_some_and(|entry| entry.content_security_policy_inherited()); - let has_own_response_policies = self - .child_browsing_contexts - .get(&child_handle) - .is_some_and(|entry| entry.has_response_content_security_policies()); - if inherits_parent && !has_own_response_policies { - return self.parent_effective_content_security_reporting_endpoints(child_handle); - } self.child_browsing_contexts .get(&child_handle) .map(|entry| entry.content_security_reporting_endpoints()) .unwrap_or_default() } - fn parent_effective_content_security_reporting_endpoints( - &self, - child_handle: DomHandle, - ) -> ContentSecurityPolicyReportingEndpoints { - match self.child_browsing_context_parent_handle(child_handle) { - Some(parent) => self.child_effective_content_security_reporting_endpoints(parent), - None => unsafe { &*self.runtime } - .content_security_reporting_endpoints() - .clone(), - } - } - pub(crate) fn allows_eval_code_generation_by_csp<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, @@ -921,15 +802,15 @@ impl JsContextHost { check: DocumentContentSecurityPolicyCheck, include_call_location: bool, ) -> bool { - let (mut report_only_violation, mut enforced_violation) = check.into_violations(); - if report_only_violation.is_none() && enforced_violation.is_none() { + let (mut report_only_violations, mut enforced_violations) = check.into_violations(); + if report_only_violations.is_empty() && enforced_violations.is_empty() { return true; } if include_call_location && let Some((source_file, line_number, column_number)) = current_script_violation_location(scope) { - for violation in [&mut report_only_violation, &mut enforced_violation] + for violation in [&mut report_only_violations, &mut enforced_violations] .into_iter() .flatten() { @@ -939,18 +820,17 @@ impl JsContextHost { } } let host_ptr: *mut JsContextHost = self; - if let Some(violation) = report_only_violation { + for violation in report_only_violations { self.dispatch_content_security_policy_violation_event_for_owner_best_effort( scope, host_ptr, owner, &violation, ); } - let Some(violation) = enforced_violation else { - return true; - }; - self.dispatch_content_security_policy_violation_event_for_owner_best_effort( - scope, host_ptr, owner, &violation, - ); - false + for violation in &enforced_violations { + self.dispatch_content_security_policy_violation_event_for_owner_best_effort( + scope, host_ptr, owner, violation, + ); + } + enforced_violations.is_empty() } pub(crate) fn child_wasm_eval_csp_violation( @@ -964,6 +844,8 @@ impl JsContextHost { )? .into_violations() .1 + .into_iter() + .next() } pub(crate) fn allows_trusted_type_policy_name_by_csp<'s>( diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 52a504207..ba6c6de48 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -4530,7 +4530,7 @@ impl PageVm { ); } - let (_, enforced_violation) = self + let (_, enforced_violations) = self .vm() .document_runtime .style_element_request_csp_check( @@ -4540,7 +4540,7 @@ impl PageVm { }, ) .into_violations(); - if enforced_violation.is_some() { + if !enforced_violations.is_empty() { return ScannedStylesheetAdmission::DeferredToParser( ScannedStylesheetDeferral::ContentSecurityPolicy, ); @@ -4577,7 +4577,7 @@ impl PageVm { ScannedImageDeferral::FetchInterception, ); } - let (_, enforced_violation) = self + let (_, enforced_violations) = self .vm() .document_runtime .document_subresource_csp_check( @@ -4585,7 +4585,7 @@ impl PageVm { crate::document_runtime::DocumentSubresourceCspKind::Image, ) .into_violations(); - if enforced_violation.is_some() { + if !enforced_violations.is_empty() { return ScannedImageAdmission::DeferredToParser( ScannedImageDeferral::ContentSecurityPolicy, ); diff --git a/moli-renderer-v8/src/script_vm/app_manifest.rs b/moli-renderer-v8/src/script_vm/app_manifest.rs index 463f1e9d0..3b099737c 100644 --- a/moli-renderer-v8/src/script_vm/app_manifest.rs +++ b/moli-renderer-v8/src/script_vm/app_manifest.rs @@ -45,11 +45,11 @@ impl ScriptVm { return complete_default_app_manifest(&document_url, Some(&manifest_url)); } - let (_report_only_violation, enforced_violation) = self + let (_report_only_violation, enforced_violations) = self .document_runtime .document_subresource_csp_check(&manifest_url, DocumentSubresourceCspKind::Manifest) .into_violations(); - if enforced_violation.is_some() { + if !enforced_violations.is_empty() { return complete_default_app_manifest(&document_url, Some(&manifest_url)); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs index bedb00618..374195157 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs @@ -36,6 +36,72 @@ fn inline_script_and_handler_csp_accept_base64url_hashes() { } } +#[test] +fn inline_script_reports_every_policy_and_only_enforced_policies_block() { + let policies = [ + "script-src 'nonce-allowed' 'report-sample'".to_owned(), + "script-src 'nonce-allowed'".to_owned(), + ]; + for enforce in [false, true] { + let mut vm = new_storage_test_vm("https://multiple-inline-csp.test/page.html"); + if enforce { + vm.set_response_content_security_policies(&policies); + } + vm.set_response_content_security_report_only_policies(&policies); + vm.eval( + r#" +globalThis.multipleCspEvents = []; +document.addEventListener('securitypolicyviolation', event => { + multipleCspEvents.push({ + policy: event.originalPolicy, + disposition: event.disposition, + directive: event.effectiveDirective, + blockedURI: event.blockedURI, + sample: event.sample, + target: event.target.id, + }); +}); +const root = document.documentElement || document.appendChild(document.createElement('html')); +const blocked = document.createElement('script'); +blocked.id = 'reported'; +blocked.text = 'globalThis.untrustedRan = true'; +root.appendChild(blocked); +const allowed = document.createElement('script'); +allowed.nonce = 'allowed'; +allowed.text = 'globalThis.trustedRan = true'; +root.appendChild(allowed); +"#, + ) + .expect("inline script probe"); + drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm); + assert_eq!( + vm.eval("globalThis.untrustedRan === true").unwrap(), + (!enforce).to_string(), + ); + assert_eq!(vm.eval("globalThis.trustedRan === true").unwrap(), "true"); + let events: serde_json::Value = + serde_json::from_str(&vm.eval("JSON.stringify(multipleCspEvents)").unwrap()).unwrap(); + let mut expected = Vec::new(); + for disposition in if enforce { + vec!["report", "enforce"] + } else { + vec!["report"] + } { + for (index, policy) in policies.iter().enumerate() { + expected.push(serde_json::json!({ + "policy": policy, + "disposition": disposition, + "directive": "script-src-elem", + "blockedURI": "inline", + "sample": if index == 0 { "globalThis.untrustedRan = true" } else { "" }, + "target": "reported", + })); + } + } + assert_eq!(events, serde_json::json!(expected)); + } +} + #[test] fn module_fetch_csp_uses_captured_parser_metadata_and_nonce() { let mut vm = new_storage_test_vm("https://module-csp-provenance.test/page.html"); diff --git a/moli-renderer-v8/src/stylesheet_runtime/blocking.rs b/moli-renderer-v8/src/stylesheet_runtime/blocking.rs index 883bea07c..15d5e4f23 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/blocking.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/blocking.rs @@ -43,7 +43,7 @@ impl DocumentRuntime { request_resource_type: moli_fetch::RequestResourceType, link_preload: bool, ) -> Result { - let (_, enforced_violation) = self + let (_, enforced_violations) = self .response_style_element_request_csp_check( &request_url, crate::content_security_policy::ContentSecurityPolicyStyleElementRequest { @@ -51,7 +51,7 @@ impl DocumentRuntime { }, ) .into_violations(); - if enforced_violation.is_some() { + if !enforced_violations.is_empty() { // The eventual DOM client owns violation reporting and its // load/error event. Speculation only decides whether a physical // resource may start.