From 2e290b0be9af04bbc7def2329099d27f7845f69a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 21:59:36 +0800 Subject: [PATCH] fix(trusted-types): report every violated CSP policy Report each enforced and report-only Trusted Types policy independently, preserving per-policy endpoints and source locations. Queue worker TT and WebSocket CSP reports without synchronous reentry or overtaking earlier reports. Honor report-only Trusted Types requirements for worker importScripts and string timers. Cover duplicate policies, default-policy conversion, trusted values, asynchronous delivery, and cross-directive ordering. Validation: cargo fmt --all; workspace clippy with all targets/features and -D warnings; full nextest (17726 passed, 13 skipped). All 219 Trusted Types WPT cases compared in CLI and CDP: 191 to 195 passed, 9 additional passing subtests, no regressions. Promote the four verified cases in the baseline lists. --- .../src/content_security_policy.rs | 90 +++--- .../src/document_runtime/security_policy.rs | 111 ++++++-- .../context_host/security_policy.rs | 52 ++-- .../tests/browser_api/trusted_types.rs | 72 +++++ .../global_scope/content_security_policy.rs | 144 +++++----- .../src/worker/global_scope/fetch.rs | 53 ++-- .../src/worker/global_scope/mod.rs | 9 +- .../src/worker/global_scope/timers.rs | 10 +- .../worker/thread/tests/imported_scripts.rs | 18 +- .../src/worker/thread/tests/mod.rs | 2 +- .../src/worker/thread/tests/network.rs | 9 +- .../thread/tests/trusted_types_reporting.rs | 267 ++++++++++++++++++ 12 files changed, 621 insertions(+), 216 deletions(-) create mode 100644 moli-renderer-v8/src/worker/thread/tests/trusted_types_reporting.rs diff --git a/moli-renderer-v8/src/content_security_policy.rs b/moli-renderer-v8/src/content_security_policy.rs index c26b3c6aad..0079db3739 100644 --- a/moli-renderer-v8/src/content_security_policy.rs +++ b/moli-renderer-v8/src/content_security_policy.rs @@ -997,72 +997,62 @@ pub(crate) fn current_script_violation_location( } pub(crate) fn content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints( - policies: &[String], + policy: &str, protected_url: &Url, sink: &str, sample: &str, disposition: ContentSecurityPolicyDisposition, reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, ) -> Option { - policies.iter().find_map(|policy| { - if !policy_requires_trusted_types_for_script(policy) { - return None; - } - let document_uri = protected_url.to_string(); - Some(ContentSecurityPolicyUrlViolation { - effective_directive: REQUIRE_TRUSTED_TYPES_FOR, - blocked_uri: "trusted-types-sink".to_owned(), - source_file: document_uri.clone(), - document_uri, - original_policy: policy.clone(), - disposition, - report_uri_endpoints: content_security_policy_report_uri_endpoints( - policy, - protected_url, - ), - report_to_endpoints: content_security_policy_report_to_endpoints( - policy, - reporting_endpoints, - ), - sample: trusted_types_sink_violation_sample(sink, sample), - line_number: 0, - column_number: 0, - }) + if !policy_requires_trusted_types_for_script(policy) { + return None; + } + let document_uri = protected_url.to_string(); + Some(ContentSecurityPolicyUrlViolation { + effective_directive: REQUIRE_TRUSTED_TYPES_FOR, + blocked_uri: "trusted-types-sink".to_owned(), + source_file: document_uri.clone(), + document_uri, + original_policy: policy.to_owned(), + disposition, + report_uri_endpoints: content_security_policy_report_uri_endpoints(policy, protected_url), + report_to_endpoints: content_security_policy_report_to_endpoints( + policy, + reporting_endpoints, + ), + sample: trusted_types_sink_violation_sample(sink, sample), + line_number: 0, + column_number: 0, }) } pub(crate) fn content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( - policies: &[String], + policy: &str, protected_url: &Url, policy_name: &str, is_duplicate: bool, disposition: ContentSecurityPolicyDisposition, reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, ) -> Option { - policies.iter().find_map(|policy| { - if policy_allows_trusted_type_policy_name(policy, policy_name, is_duplicate) { - return None; - } - let document_uri = protected_url.to_string(); - Some(ContentSecurityPolicyUrlViolation { - effective_directive: TRUSTED_TYPES, - blocked_uri: "trusted-types-policy".to_owned(), - source_file: document_uri.clone(), - document_uri, - original_policy: policy.clone(), - disposition, - report_uri_endpoints: content_security_policy_report_uri_endpoints( - policy, - protected_url, - ), - report_to_endpoints: content_security_policy_report_to_endpoints( - policy, - reporting_endpoints, - ), - sample: trusted_types_violation_sample(policy_name), - line_number: 0, - column_number: 0, - }) + if policy_allows_trusted_type_policy_name(policy, policy_name, is_duplicate) { + return None; + } + let document_uri = protected_url.to_string(); + Some(ContentSecurityPolicyUrlViolation { + effective_directive: TRUSTED_TYPES, + blocked_uri: "trusted-types-policy".to_owned(), + source_file: document_uri.clone(), + document_uri, + original_policy: policy.to_owned(), + disposition, + report_uri_endpoints: content_security_policy_report_uri_endpoints(policy, protected_url), + report_to_endpoints: content_security_policy_report_to_endpoints( + policy, + reporting_endpoints, + ), + sample: trusted_types_violation_sample(policy_name), + line_number: 0, + column_number: 0, }) } diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index cdc61c8041..7441954a5c 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -1466,7 +1466,7 @@ impl DocumentRuntime { } #[allow(clippy::too_many_arguments)] - pub(crate) fn trusted_type_policy_name_csp_check_for_document( + pub(crate) fn trusted_type_policy_name_csp_violations_for_document( &self, document_handle: Option, document_url: &Url, @@ -1475,7 +1475,7 @@ impl DocumentRuntime { response_reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, policy_name: &str, is_duplicate: bool, - ) -> DocumentContentSecurityPolicyCheck { + ) -> Vec { let enforced_policies = self .document_content_security_policy_strings_for_optional_document( document_handle, @@ -1486,22 +1486,21 @@ impl DocumentRuntime { response_report_only_policies, response_reporting_endpoints, ); - DocumentContentSecurityPolicyCheck { - report_only_violation: document_trusted_types_policy_violation_from_document_policies( - report_only_policies, - document_url, - policy_name, - is_duplicate, - ContentSecurityPolicyDisposition::Report, - ), - enforced_violation: document_trusted_types_policy_violation_from_document_policies( - enforced_policies, - document_url, - policy_name, - is_duplicate, - ContentSecurityPolicyDisposition::Enforce, - ), - } + iter_document_trusted_types_policy_violations( + enforced_policies, + document_url, + policy_name, + is_duplicate, + ContentSecurityPolicyDisposition::Enforce, + ) + .chain(iter_document_trusted_types_policy_violations( + report_only_policies, + document_url, + policy_name, + is_duplicate, + ContentSecurityPolicyDisposition::Report, + )) + .collect() } pub(crate) fn queue_content_security_policy_violation_event_best_effort<'s>( @@ -2088,10 +2087,9 @@ fn iter_document_trusted_types_sink_policy_violations<'a>( disposition: ContentSecurityPolicyDisposition, ) -> impl Iterator + 'a { policies.into_iter().filter_map(move |policy| { - let single_policy = [policy.policy.clone()]; let mut violation = content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints( - &single_policy, + &policy.policy, document_url, sink, sample, @@ -2103,18 +2101,17 @@ fn iter_document_trusted_types_sink_policy_violations<'a>( }) } -fn document_trusted_types_policy_violation_from_document_policies( +fn iter_document_trusted_types_policy_violations<'a>( policies: Vec, - document_url: &Url, - policy_name: &str, + document_url: &'a Url, + policy_name: &'a str, is_duplicate: bool, disposition: ContentSecurityPolicyDisposition, -) -> Option { - policies.into_iter().find_map(|policy| { - let single_policy = [policy.policy.clone()]; +) -> impl Iterator + 'a { + policies.into_iter().filter_map(move |policy| { let mut violation = content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( - &single_policy, + &policy.policy, document_url, policy_name, is_duplicate, @@ -2286,6 +2283,66 @@ mod tests { ); } + #[test] + fn trusted_types_policy_reports_preserve_each_policy_endpoint_and_meta_suppression() { + let runtime = runtime_for_html( + r#" + "#, + ); + let enforced = [ + "trusted-types 'none'; report-uri /first", + "trusted-types 'none'; report-uri /second", + ] + .map(str::to_owned); + let report_only = [ + "trusted-types * 'allow-duplicates'; report-uri /allowed", + "trusted-types 'none'; report-uri /report", + ] + .map(str::to_owned); + let reports = runtime.trusted_type_policy_name_csp_violations_for_document( + Some(runtime.document_handle()), + runtime.document_url(), + &enforced, + &report_only, + &Default::default(), + &"p".repeat(50), + false, + ); + assert_eq!(reports.len(), 4); + let endpoints: Vec<_> = reports + .iter() + .map(|report| { + report + .report_uri_endpoints + .iter() + .map(ToString::to_string) + .collect::>() + }) + .collect(); + assert_eq!( + endpoints, + vec![ + vec!["https://example.test/first"], + vec!["https://example.test/second"], + vec![], + vec!["https://example.test/report"], + ] + ); + for (index, report) in reports.iter().enumerate() { + assert_eq!(report.sample, "p".repeat(40)); + assert_eq!(report.blocked_uri, "trusted-types-policy"); + assert_eq!( + report.disposition, + if index == 3 { + ContentSecurityPolicyDisposition::Report + } else { + ContentSecurityPolicyDisposition::Enforce + } + ); + } + } + #[test] fn response_csp_sandbox_updates_current_document_sandbox_policy_record() { let mut runtime = runtime_for_html(""); 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 22c8c48985..3108166bac 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 @@ -1,10 +1,10 @@ use super::{JsContextHost, OwnerDispatchScope}; use crate::{ content_security_policy::{ - ContentSecurityPolicyNonUrlKind, ContentSecurityPolicyRedirectStatus, - ContentSecurityPolicyReportingEndpoints, ContentSecurityPolicyScriptElementRequest, - ContentSecurityPolicyViolationEventFields, TrustedTypesForScriptRequirements, - current_script_violation_location, + ContentSecurityPolicyDisposition, ContentSecurityPolicyNonUrlKind, + ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyReportingEndpoints, + ContentSecurityPolicyScriptElementRequest, ContentSecurityPolicyViolationEventFields, + TrustedTypesForScriptRequirements, current_script_violation_location, }, context_bootstrap::CHILD_BROWSING_CONTEXT_HANDLE_SLOT, document_runtime::{ @@ -914,44 +914,38 @@ impl JsContextHost { return true; }; // SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime. - let check = unsafe { &*self.runtime }.trusted_type_policy_name_csp_check_for_document( - snapshot.document_handle, - &snapshot.document_url, - &snapshot.policy_container.response_content_security_policies, - &snapshot - .policy_container - .response_content_security_report_only_policies, - &snapshot - .policy_container - .content_security_reporting_endpoints, - policy_name, - is_duplicate, - ); - let (mut report_only_violation, mut enforced_violation) = check.into_violations(); + let mut violations = unsafe { &*self.runtime } + .trusted_type_policy_name_csp_violations_for_document( + snapshot.document_handle, + &snapshot.document_url, + &snapshot.policy_container.response_content_security_policies, + &snapshot + .policy_container + .response_content_security_report_only_policies, + &snapshot + .policy_container + .content_security_reporting_endpoints, + policy_name, + is_duplicate, + ); if !self.active_inspector_dispatch && let Some((source_file, line_number, column_number)) = current_script_violation_location(scope) { - for violation in [&mut enforced_violation, &mut report_only_violation] - .into_iter() - .flatten() - { + for violation in &mut violations { violation.source_file.clone_from(&source_file); violation.line_number = line_number; violation.column_number = column_number; } } let host_ptr: *mut JsContextHost = self; - let allowed = enforced_violation.is_none(); + let allowed = !violations + .iter() + .any(|violation| violation.disposition == ContentSecurityPolicyDisposition::Enforce); // Policy creation reports expose CSP list ordering. Response policy // state is partitioned by disposition, with enforce policies modeled // before report-only policies, so preserve that order here. - if let Some(violation) = enforced_violation { - self.dispatch_content_security_policy_violation_event_for_owner_best_effort( - scope, host_ptr, owner, &violation, - ); - } - if let Some(violation) = report_only_violation { + for violation in violations { self.dispatch_content_security_policy_violation_event_for_owner_best_effort( scope, host_ptr, owner, &violation, ); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index a9c99827d7..fda3109808 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -433,6 +433,78 @@ fn trusted_type_policy_creation_reports_name_and_duplicate_csp_violations() { ); } +#[test] +fn trusted_type_policy_creation_reports_every_violated_policy() { + let enforced = [ + "trusted-types allowed duplicate reportOnly", + "trusted-types allowed duplicate reportOnly", + "trusted-types allowed duplicate reportOnly 'allow-duplicates'", + ] + .map(str::to_owned); + let report_only = [ + "trusted-types allowed duplicate", + "trusted-types allowed duplicate", + "trusted-types allowed duplicate 'allow-duplicates'", + ] + .map(str::to_owned); + + for enforce in [false, true] { + let mut vm = new_storage_test_vm("https://trusted-type-policy-csp.test/"); + if enforce { + vm.set_response_content_security_policies(&enforced); + } + vm.set_response_content_security_report_only_policies(&report_only); + let result = vm + .eval( + r#" +globalThis.policyReports = []; +document.addEventListener('securitypolicyviolation', event => { + policyReports.push([event.sample, event.disposition, event.originalPolicy]); +}); +JSON.stringify(['allowed', 'reportOnly', 'duplicate', 'duplicate', 'blocked'].map(name => { + try { return trustedTypes.createPolicy(name).name; } + catch (error) { return error.name; } +})); +"#, + ) + .unwrap(); + assert_eq!( + result, + if enforce { + r#"["allowed","reportOnly","duplicate","TypeError","TypeError"]"# + } else { + r#"["allowed","reportOnly","duplicate","duplicate","blocked"]"# + } + ); + assert_eq!(vm.eval("policyReports.length").unwrap(), "0"); + + let mut expected = Vec::new(); + for (name, count) in [("reportOnly", 3), ("duplicate", 2), ("blocked", 3)] { + if enforce && name != "reportOnly" { + expected.extend( + enforced[..count] + .iter() + .map(|policy| serde_json::json!([name, "enforce", policy])), + ); + } + expected.extend( + report_only[..count] + .iter() + .map(|policy| serde_json::json!([name, "report", policy])), + ); + } + assert_eq!( + drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm), + expected.len() + ); + let actual = vm.eval("JSON.stringify(policyReports)").unwrap(); + assert_eq!( + serde_json::from_str::(&actual).unwrap(), + serde_json::json!(expected) + ); + } +} + #[test] fn element_markup_sinks_enforce_trusted_html_and_standard_sink_names() { let mut vm = new_storage_test_vm("https://element-markup-trusted-types.test/"); diff --git a/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs b/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs index 9fdc34f28e..82b67f9948 100644 --- a/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs +++ b/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs @@ -28,7 +28,7 @@ use crate::service_worker_runtime::{ }; use crate::types::{AsyncSubresourceNetworkContext, SubresourceResourceType}; use crate::worker::handle::WorkerPendingSubresourceFetch; -use crate::worker::{WorkerPendingFetchContinue, WorkerToParentMessage}; +use crate::worker::{WorkerMessage, WorkerPendingFetchContinue, WorkerToParentMessage}; use moli_fetch::{ BrowserRequestMetadata, FetchCancelHandle, Request, RequestResourceType, should_request_be_blocked_due_to_bad_port, @@ -92,30 +92,25 @@ pub(super) fn dispatch_worker_trusted_types_sink_violation_event_for_state<'s>( sink: &str, sample: &str, ) { - let violation = { + let violations = { let state_ref = state.borrow(); let Some(protected_url) = state_ref.current_script_url.as_ref() else { return; }; - content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints( - &state_ref.content_security_policies, - protected_url, - sink, - sample, - ContentSecurityPolicyDisposition::Enforce, - &state_ref.content_security_reporting_endpoints, - ) + trusted_types_policies(&state_ref) + .filter_map(|(policy, disposition)| { + content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints( + policy, + protected_url, + sink, + sample, + disposition, + &state_ref.content_security_reporting_endpoints, + ) + }) + .collect() }; - if let Some(mut violation) = violation { - if let Some((source_file, line_number, column_number)) = - current_script_violation_location(scope) - { - violation.source_file = source_file; - violation.line_number = line_number; - violation.column_number = column_number; - } - dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation); - } + queue_worker_trusted_types_violations(scope, state, violations); } pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>( @@ -124,58 +119,79 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>( policy_name: &str, is_duplicate: bool, ) -> bool { - let (mut report_only_violation, mut enforced_violation) = { + let violations: Vec<_> = { let state_ref = state.borrow(); let Some(protected_url) = state_ref.current_script_url.as_ref() else { return true; }; - let report_only_violation = - content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( - &state_ref.content_security_report_only_policies, - protected_url, - policy_name, - is_duplicate, - ContentSecurityPolicyDisposition::Report, - &state_ref.content_security_reporting_endpoints, - ); - let enforced_violation = - content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( - &state_ref.content_security_policies, - protected_url, - policy_name, - is_duplicate, - ContentSecurityPolicyDisposition::Enforce, - &state_ref.content_security_reporting_endpoints, - ); - (report_only_violation, enforced_violation) + trusted_types_policies(&state_ref) + .filter_map(|(policy, disposition)| { + content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( + policy, + protected_url, + policy_name, + is_duplicate, + disposition, + &state_ref.content_security_reporting_endpoints, + ) + }) + .collect() }; - let allowed = enforced_violation.is_none(); - if allowed && report_only_violation.is_none() { - return true; - } - if let Some((source_file, line_number, column_number)) = - current_script_violation_location(scope) - { - for violation in [&mut report_only_violation, &mut enforced_violation] - .into_iter() - .flatten() - { - violation.source_file = source_file.clone(); - violation.line_number = line_number; - violation.column_number = column_number; - } - } - // Match window policy creation reporting: enforce response policies are - // modeled before report-only policies in the partitioned policy state. - if let Some(violation) = enforced_violation { - dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation); - } - if let Some(violation) = report_only_violation { - dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation); - } + let allowed = !violations + .iter() + .any(|violation| violation.disposition == ContentSecurityPolicyDisposition::Enforce); + queue_worker_trusted_types_violations(scope, state, violations); allowed } +fn trusted_types_policies( + state: &WorkerGlobalState, +) -> impl Iterator { + // Preserve the same partition ordering as document reporting. Identical + // policies remain distinct entries and each can produce a report. + [ + ( + &state.content_security_policies, + ContentSecurityPolicyDisposition::Enforce, + ), + ( + &state.content_security_report_only_policies, + ContentSecurityPolicyDisposition::Report, + ), + ] + .into_iter() + .flat_map(|(policies, disposition)| { + policies + .iter() + .map(move |policy| (policy.as_str(), disposition)) + }) +} + +fn queue_worker_trusted_types_violations( + scope: &mut v8::PinScope<'_, '_>, + state: &Rc>, + violations: Vec, +) { + if violations.is_empty() { + return; + } + // Capture the caller before returning from the sink, but deliver events + // in a later task. In particular, a listener must not reenter createPolicy + // before the original call has recorded the newly created policy name. + let location = current_script_violation_location(scope); + let wake_tx = state.borrow().worker_wake_tx.clone(); + for mut violation in violations { + if let Some((source_file, line_number, column_number)) = &location { + violation.source_file.clone_from(source_file); + violation.line_number = *line_number; + violation.column_number = *column_number; + } + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + } +} + fn create_worker_content_security_policy_violation_event<'s>( scope: &mut v8::PinScope<'s, '_>, violation: &ContentSecurityPolicyUrlViolation, diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 9f7c8042bb..0dccc4968c 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -1294,32 +1294,41 @@ pub(crate) fn check_worker_websocket_csp( url: &Url, ) -> Option { let state = get_worker_state(scope)?; - dispatch_worker_content_security_policy_report_only_violation_for_state( - scope, - &state, - document_url, - url, - crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConnect, - ); - let violation = { + let (wake_tx, report_only_violation, enforced_violation) = { let state_ref = state.borrow(); - worker_content_security_policy_violation( - &state_ref, - document_url, - url, - crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConnect, + ( + state_ref.worker_wake_tx.clone(), + worker_content_security_policy_report_only_violation( + &state_ref, + document_url, + url, + crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConnect, + ), + worker_content_security_policy_violation( + &state_ref, + document_url, + url, + crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConnect, + ), ) }; - Some(match violation { - Some(violation) => { - let message = worker_content_security_policy_error_message(&violation, "WebSocket"); - dispatch_worker_content_security_policy_violation_event_for_state( - scope, &state, &violation, - ); - WorkerWebSocketCspOutcome::Blocked(message) - } + let outcome = match &enforced_violation { + Some(violation) => WorkerWebSocketCspOutcome::Blocked( + worker_content_security_policy_error_message(violation, "WebSocket"), + ), None => WorkerWebSocketCspOutcome::Allowed, - }) + }; + // CSP reports are tasks, including those produced by WebSocket construction. + // They must not overtake earlier queued Trusted Types violation reports. + for violation in [report_only_violation, enforced_violation] + .into_iter() + .flatten() + { + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + } + Some(outcome) } pub(crate) fn register_worker_websocket<'s>( diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 588d84e859..0a141fbdc3 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -6639,18 +6639,13 @@ fn worker_import_scripts_callback<'s>( throw_type_error(scope, "Module scripts don't support importScripts()."); return; } - let require_trusted_types_for_script = - crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( - &state.borrow().content_security_policies, - ); + let requirements = worker_trusted_types_for_script_requirements(scope).unwrap_or_default(); let mut prepared = Vec::with_capacity(args.length() as usize); for i in 0..args.length() { let Some(specifier) = crate::context_bootstrap::trusted_script_url_string_or_throw( scope, args.get(i), - crate::content_security_policy::TrustedTypesForScriptRequirements::enforced_only( - require_trusted_types_for_script, - ), + requirements, "WorkerGlobalScope importScripts", "importScripts", ) else { diff --git a/moli-renderer-v8/src/worker/global_scope/timers.rs b/moli-renderer-v8/src/worker/global_scope/timers.rs index adfd0989c6..fc883a3c9e 100644 --- a/moli-renderer-v8/src/worker/global_scope/timers.rs +++ b/moli-renderer-v8/src/worker/global_scope/timers.rs @@ -125,11 +125,7 @@ fn worker_timer_callback_from_arg<'s>( return Ok(Some(WorkerTimerCallback::webidl_timer(scope, callback))); } - let require_trusted_types_for_script = get_worker_state(scope).is_some_and(|state| { - crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( - &state.borrow().content_security_policies, - ) - }); + let requirements = worker_trusted_types_for_script_requirements(scope).unwrap_or_default(); let sink = match timer_name { "setInterval" => "WorkerGlobalScope setInterval", _ => "WorkerGlobalScope setTimeout", @@ -137,9 +133,7 @@ fn worker_timer_callback_from_arg<'s>( let Some(source) = crate::context_bootstrap::trusted_script_string_or_type_error( scope, value, - crate::content_security_policy::TrustedTypesForScriptRequirements::enforced_only( - require_trusted_types_for_script, - ), + requirements, sink, timer_name, ) else { diff --git a/moli-renderer-v8/src/worker/thread/tests/imported_scripts.rs b/moli-renderer-v8/src/worker/thread/tests/imported_scripts.rs index a394c955bc..7113d0bb55 100644 --- a/moli-renderer-v8/src/worker/thread/tests/imported_scripts.rs +++ b/moli-renderer-v8/src/worker/thread/tests/imported_scripts.rs @@ -16,15 +16,17 @@ async fn worker_importscripts_trusted_types_reports_keep_script_and_document_loc let options = WorkerSpawnOptions::new( r#" const violations = []; - addEventListener('securitypolicyviolation', event => violations.push([ + addEventListener('securitypolicyviolation', event => { + violations.push([ event.effectiveDirective, event.documentURI, event.sourceFile, event.lineNumber, event.columnNumber > 0 - ])); + ]); + if (violations.length === 4) { postMessage(violations); close(); } + }); const setup = trustedTypes.createPolicy('bootstrap', { createScriptURL: s => s }); importScripts(setup.createScriptURL('../imported/violations.js')); setTimeout(() => { violatePolicy(); violateSink(); - postMessage(violations); close(); }, 0); "# .into(), @@ -56,7 +58,8 @@ async fn worker_importscripts_trusted_types_reports_do_not_expose_redirect_targe "/private-user/violations.js?credential=hidden", "HTTP/1.1 200 OK", "text/javascript", - "setTimeout(() => {\n try { trustedTypes.createPolicy('forbidden'); } catch {}\n postMessage(violations); close();\n}, 0);".into(), + "setTimeout(() => {\n try { trustedTypes.createPolicy('forbidden'); } catch {}\n}, 0);" + .into(), Duration::ZERO, )]) .await; @@ -73,10 +76,13 @@ async fn worker_importscripts_trusted_types_reports_do_not_expose_redirect_targe let options = WorkerSpawnOptions::new( r#" const violations = []; - addEventListener('securitypolicyviolation', event => violations.push([ + addEventListener('securitypolicyviolation', event => { + violations.push([ event.disposition, event.documentURI, event.sourceFile, event.lineNumber, event.columnNumber > 0 - ])); + ]); + if (violations.length === 2) { postMessage(violations); close(); } + }); importScripts('./redirect.js'); "# .into(), diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index 75f15c0ffd..4ca02fda89 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -1536,5 +1536,5 @@ mod strict_script_mime; mod network; mod postmessage; mod tls; - +mod trusted_types_reporting; mod xhr_failure; diff --git a/moli-renderer-v8/src/worker/thread/tests/network.rs b/moli-renderer-v8/src/worker/thread/tests/network.rs index 8d12d48d98..97c9526285 100644 --- a/moli-renderer-v8/src/worker/thread/tests/network.rs +++ b/moli-renderer-v8/src/worker/thread/tests/network.rs @@ -5935,6 +5935,8 @@ async fn worker_websocket_csp_block_precedes_mixed_content_rejection() { effectiveDirective: event.effectiveDirective, disposition: event.disposition }); + postMessage({ outcome, events }); + close(); }); let outcome; try { @@ -5943,8 +5945,7 @@ async fn worker_websocket_csp_block_precedes_mixed_content_rejection() { } catch (error) { outcome = `throw:${error.name}`; } - postMessage({ outcome, events }); - close(); + postMessage({ outcome, events: events.length }); "# .to_owned(), "http://localhost:8000/worker/main.js".to_owned(), @@ -5952,6 +5953,10 @@ async fn worker_websocket_csp_block_precedes_mixed_content_rejection() { .with_content_security_policies(vec!["connect-src 'none'".to_owned()]), ); + assert_eq!( + recv_post_json(&mut handle).await, + r#"{"outcome":"socket:0:ws://common/blank.html","events":0}"# + ); assert_eq!( recv_post_json(&mut handle).await, r#"{"outcome":"socket:0:ws://common/blank.html","events":[{"blockedURI":"ws://common/blank.html","effectiveDirective":"connect-src","disposition":"enforce"}]}"# diff --git a/moli-renderer-v8/src/worker/thread/tests/trusted_types_reporting.rs b/moli-renderer-v8/src/worker/thread/tests/trusted_types_reporting.rs new file mode 100644 index 0000000000..ee17c3a56f --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/trusted_types_reporting.rs @@ -0,0 +1,267 @@ +use super::*; + +#[tokio::test] +async fn worker_trusted_types_policy_creation_queues_every_violated_policy() { + ensure_v8(); + let enforced = [ + "trusted-types allowed duplicate reportOnly", + "trusted-types allowed duplicate reportOnly", + "trusted-types allowed duplicate reportOnly 'allow-duplicates'", + ] + .map(str::to_owned); + let report_only = [ + "trusted-types allowed duplicate", + "trusted-types allowed duplicate", + "trusted-types allowed duplicate 'allow-duplicates'", + ] + .map(str::to_owned); + for enforce in [false, true] { + let mut expected = Vec::new(); + for (name, count) in [("reportOnly", 3), ("duplicate", 2), ("blocked", 3)] { + if enforce && name != "reportOnly" { + expected.extend( + enforced[..count] + .iter() + .map(|policy| serde_json::json!([name, "enforce", policy])), + ); + } + expected.extend( + report_only[..count] + .iter() + .map(|policy| serde_json::json!([name, "report", policy])), + ); + } + let script = format!( + r#" +const reports = []; +addEventListener('securitypolicyviolation', event => {{ + reports.push([event.sample, event.disposition, event.originalPolicy]); + if (reports.length === {}) {{ postMessage(reports); close(); }} +}}); +const names = ['allowed', 'reportOnly', 'duplicate', 'duplicate', 'blocked'].map(name => {{ + try {{ return trustedTypes.createPolicy(name).name; }} + catch (error) {{ return error.name; }} +}}); +postMessage({{names, reports: reports.length}}); +"#, + expected.len() + ); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new(script, "https://app.test/worker/main.js".into()) + .with_content_security_policies(if enforce { enforced.to_vec() } else { vec![] }) + .with_content_security_report_only_policies(report_only.to_vec()), + ); + let sync = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap(); + assert_eq!( + expect_post_json(sync), + if enforce { + r#"{"names":["allowed","reportOnly","duplicate","TypeError","TypeError"],"reports":0}"# + } else { + r#"{"names":["allowed","reportOnly","duplicate","duplicate","blocked"],"reports":0}"# + } + ); + let reports = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap(); + let actual: serde_json::Value = serde_json::from_str(&expect_post_json(reports)).unwrap(); + assert_eq!(actual, serde_json::json!(expected)); + } +} + +#[tokio::test] +async fn worker_trusted_types_sink_reports_cover_every_policy_and_report_only_execution() { + ensure_v8(); + let policies = [ + "require-trusted-types-for 'script'", + "require-trusted-types-for 'invalid' 'script'", + "require-trusted-types-for 'script'", + ] + .map(str::to_owned); + for enforce in [false, true] { + for (operation, error, sample) in [ + ( + "eval('globalThis.ran = true')", + "EvalError", + "eval|globalThis.ran = true", + ), + ( + "clearTimeout(setTimeout('globalThis.ran = true'))", + "TypeError", + "WorkerGlobalScope setTimeout|globalThis.ran = true", + ), + ( + "clearInterval(setInterval('globalThis.ran = true'))", + "TypeError", + "WorkerGlobalScope setInterval|globalThis.ran = true", + ), + ( + "importScripts('data:text/javascript,globalThis.ran=true')", + "TypeError", + "WorkerGlobalScope importScripts|data:text/javascript,globalThis.ran=true", + ), + ] { + let expected_count = if enforce { 6 } else { 3 }; + let script = format!( + r#" +const reports = []; +globalThis.ran = false; +addEventListener('securitypolicyviolation', event => {{ + reports.push([event.sample, event.disposition, event.originalPolicy, event.documentURI, + event.sourceFile, event.lineNumber > 0, event.columnNumber > 0]); + if (reports.length === {expected_count}) {{ postMessage(reports); close(); }} +}}); +let error = null; +try {{ {operation}; }} catch (e) {{ error = e.name; }} +postMessage({{error, ran, reports: reports.length}}); +"# + ); + let url = "https://app.test/worker/main.js"; + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new(script, url.into()) + .with_content_security_policies(if enforce { + policies.to_vec() + } else { + vec![] + }) + .with_content_security_report_only_policies(policies.to_vec()), + ); + let sync = timeout(TIMEOUT, handle.recv()) + .await + .unwrap_or_else(|error| { + panic!( + "{operation}, enforce={enforce}: waiting for synchronous result: {error}" + ) + }) + .unwrap(); + let sync: serde_json::Value = serde_json::from_str(&expect_post_json(sync)).unwrap(); + assert_eq!( + sync, + serde_json::json!({ + "error": if enforce { Some(error) } else { None }, + "ran": !enforce && !operation.starts_with("clear"), + "reports": 0, + }) + ); + let reports = timeout(TIMEOUT, handle.recv()).await + .unwrap_or_else(|error| panic!("{operation}, enforce={enforce}: waiting for {expected_count} reports: {error}")) + .unwrap(); + let actual: serde_json::Value = + serde_json::from_str(&expect_post_json(reports)).unwrap(); + let dispositions = if enforce { + vec!["enforce", "report"] + } else { + vec!["report"] + }; + let expected: Vec<_> = dispositions + .into_iter() + .flat_map(|disposition| { + policies.iter().map(move |policy| { + serde_json::json!([sample, disposition, policy, url, url, true, true,]) + }) + }) + .collect(); + assert_eq!(actual, serde_json::json!(expected), "{operation}"); + } + } +} + +#[tokio::test] +async fn worker_trusted_types_report_only_sinks_apply_default_policy_without_false_reports() { + ensure_v8(); + let policy = "require-trusted-types-for 'script'; trusted-types default named"; + for enforce in [false, true] { + let expected_count = if enforce { 2 } else { 1 }; + let script = format!( + r#" +const reports = []; +const calls = []; +addEventListener('securitypolicyviolation', event => {{ + reports.push([event.effectiveDirective, event.sample, event.disposition]); + if (reports.length === {expected_count}) {{ postMessage(reports); close(); }} +}}); +trustedTypes.createPolicy('default', {{ + createScript: (input, type, sink) => {{ calls.push([input, type, sink]); return ''; }}, + createScriptURL: (input, type, sink) => {{ + calls.push([input, type, sink]); + return 'data:text/javascript,globalThis.imported=true'; + }} +}}); +const named = trustedTypes.createPolicy('named', {{createScript: s => s, createScriptURL: s => s}}); +importScripts('input'); +clearTimeout(setTimeout('input')); +clearInterval(setInterval('input')); +importScripts(named.createScriptURL('data:text/javascript,globalThis.trustedImported=true')); +clearTimeout(setTimeout(named.createScript(''))); +clearInterval(setInterval(named.createScript(''))); +// A final policy violation provides an event-driven barrier for earlier sink reports. +try {{ trustedTypes.createPolicy('blocked'); }} catch (_) {{}} +postMessage({{calls, imported, trustedImported, reports: reports.length}}); +"# + ); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new(script, "https://app.test/worker/main.js".into()) + .with_content_security_policies(if enforce { vec![policy.into()] } else { vec![] }) + .with_content_security_report_only_policies(vec![policy.into()]), + ); + let sync = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap(); + let sync: serde_json::Value = serde_json::from_str(&expect_post_json(sync)).unwrap(); + assert_eq!( + sync, + serde_json::json!({ + "calls": [ + ["input", "TrustedScriptURL", "WorkerGlobalScope importScripts"], + ["input", "TrustedScript", "WorkerGlobalScope setTimeout"], + ["input", "TrustedScript", "WorkerGlobalScope setInterval"], + ], + "imported": true, + "trustedImported": true, + "reports": 0, + }), + ); + let reports = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap(); + let reports: serde_json::Value = serde_json::from_str(&expect_post_json(reports)).unwrap(); + let mut expected = Vec::new(); + if enforce { + expected.push(serde_json::json!(["trusted-types", "blocked", "enforce"])); + } + expected.push(serde_json::json!(["trusted-types", "blocked", "report"])); + assert_eq!(reports, serde_json::json!(expected)); + } +} + +#[tokio::test] +async fn worker_trusted_types_reports_precede_later_websocket_csp_reports() { + ensure_v8(); + let policy = "trusted-types allowed; require-trusted-types-for 'script'; connect-src 'none'"; + let script = r#" +const reports = []; +addEventListener('securitypolicyviolation', event => { + reports.push([event.effectiveDirective, event.disposition]); + if (reports.length === 6) { postMessage(reports); close(); } +}); +(async () => { + try { trustedTypes.createPolicy('blocked'); } catch (_) {} + try { eval(''); } catch (_) {} + await Promise.resolve(); + new WebSocket('ws://example.test/blocked'); + postMessage(reports.length); +})(); +"#; + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new(script.into(), "https://app.test/worker/main.js".into()) + .with_content_security_policies(vec![policy.into()]) + .with_content_security_report_only_policies(vec![policy.into()]), + ); + assert_eq!(recv_post_json(&mut handle).await, "0"); + let reports: serde_json::Value = + serde_json::from_str(&recv_post_json(&mut handle).await).unwrap(); + assert_eq!( + reports, + serde_json::json!([ + ["trusted-types", "enforce"], + ["trusted-types", "report"], + ["require-trusted-types-for", "enforce"], + ["require-trusted-types-for", "report"], + ["connect-src", "report"], + ["connect-src", "enforce"], + ]), + ); +}