From 28f1e852f2a1f0bd124cf68ec352ca575c97ba3a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 16 Jul 2026 21:59:51 +0800 Subject: [PATCH] fix(trusted-types): enforce policy creation CSP --- .../wpt-cross-current/failed-cases.txt | 8 -- .../wpt-cross-current/passed-cases.txt | 8 ++ .../src/content_security_policy.rs | 110 +++++++++++++----- .../src/context_bootstrap/trusted_types.rs | 67 ++++++++++- .../trusted_types/realm_state.rs | 1 + .../src/document_runtime/security_policy.rs | 60 ++++++++-- .../context_host/security_policy.rs | 95 ++++++++++----- .../tests/browser_api/trusted_types.rs | 66 +++++++++++ .../global_scope/content_security_policy.rs | 44 +++++++ .../src/worker/global_scope/mod.rs | 10 +- moli-renderer-v8/src/worker/mod.rs | 2 +- 11 files changed, 385 insertions(+), 86 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 23afee1484..c6f14d1c3b 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3690,8 +3690,6 @@ trusted-types/ServiceWorkerContainer-register-from-DedicatedWorker.https.html trusted-types/ServiceWorkerContainer-register-from-ServiceWorker.https.html trusted-types/ServiceWorkerContainer-register-from-SharedWorker.https.html trusted-types/TrustedType-AttributeNodes.html -trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html -trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests.html trusted-types/TrustedTypePolicyFactory-getAttributeType-namespace.html trusted-types/TrustedTypePolicyFactory-getAttributeType-svg.html trusted-types/TrustedTypePolicyFactory-getAttributeType.html @@ -3723,17 +3721,11 @@ trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-001.html trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-002.html trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-003.html trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-004-worker.html -trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-005.html -trusted-types/trusted-types-duplicate-names-list.html -trusted-types/trusted-types-duplicate-names-without-enforcement.html -trusted-types/trusted-types-duplicate-names.html trusted-types/trusted-types-eval-reporting-no-unsafe-eval.html trusted-types/trusted-types-eval-reporting-report-only.html trusted-types/trusted-types-event-handlers.html trusted-types/trusted-types-report-only.html -trusted-types/trusted-types-reporting-check-report-DedicatedWorker-create-policy.html trusted-types/trusted-types-reporting-check-report-DedicatedWorker-sink-mismatch.html -trusted-types/trusted-types-reporting-clipping-of-sample.html trusted-types/trusted-types-reporting-for-DOMParser-parseFromString.html trusted-types/trusted-types-reporting-for-DedicatedWorker-ServiceWorkerContainer-register.https.html trusted-types/trusted-types-reporting-for-Document-execCommand.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 966cdae0b5..5b1f1aa927 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -7810,7 +7810,9 @@ trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-noNamesGiven.html trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none-none-name.html trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none-none.html trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none-skip.html +trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-wildcard.html +trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests.html trusted-types/TrustedTypePolicyFactory-createPolicy-non-tt-policy-name.html trusted-types/TrustedTypePolicyFactory-createPolicy-unenforced.html trusted-types/TrustedTypePolicyFactory-defaultPolicy.html @@ -7871,8 +7873,14 @@ trusted-types/script-enforcement-017.html trusted-types/set-attributes-no-require-trusted-types.html trusted-types/should-sink-type-mismatch-violation-be-blocked-by-csp-001.html trusted-types/should-sink-type-mismatch-violation-be-blocked-by-csp-003.html +trusted-types/should-trusted-type-policy-creation-be-blocked-by-csp-005.html trusted-types/trusted-types-createHTMLDocument.html trusted-types/trusted-types-duplicate-names-list-report-only.html +trusted-types/trusted-types-duplicate-names-list.html +trusted-types/trusted-types-duplicate-names-without-enforcement.html +trusted-types/trusted-types-duplicate-names.html +trusted-types/trusted-types-reporting-check-report-DedicatedWorker-create-policy.html +trusted-types/trusted-types-reporting-clipping-of-sample.html trusted-types/trusted-types-reporting-for-DedicatedWorker-DedicatedWorker-constructor.html trusted-types/trusted-types-reporting-for-DedicatedWorker-eval.html trusted-types/trusted-types-reporting-for-DedicatedWorker-function-constructor.html diff --git a/moli-renderer-v8/src/content_security_policy.rs b/moli-renderer-v8/src/content_security_policy.rs index 865c437ea6..8d8f48bfed 100644 --- a/moli-renderer-v8/src/content_security_policy.rs +++ b/moli-renderer-v8/src/content_security_policy.rs @@ -316,15 +316,6 @@ pub(crate) fn content_security_policy_allows_trusted_types_eval(policies: &[Stri .any(|policy| policy_allows_trusted_types_eval(policy)) } -pub(crate) fn content_security_policy_allows_trusted_type_policy_name( - policies: &[String], - policy_name: &str, -) -> bool { - policies - .iter() - .all(|policy| policy_allows_trusted_type_policy_name(policy, policy_name)) -} - pub(crate) fn content_security_policy_sandboxes_document_domain(policies: &[String]) -> bool { policies .iter() @@ -972,6 +963,41 @@ pub(crate) fn content_security_policy_trusted_types_sink_violation_with_disposit }) } +pub(crate) fn content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( + policies: &[String], + 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, + }) + }) +} + pub(crate) fn content_security_policy_non_url_violation_with_disposition_and_reporting_endpoints( policy: &str, protected_url: &Url, @@ -1146,19 +1172,28 @@ fn policy_allows_trusted_types_eval(policy: &str) -> bool { }) } -fn policy_allows_trusted_type_policy_name(policy: &str, policy_name: &str) -> bool { +fn policy_allows_trusted_type_policy_name( + policy: &str, + policy_name: &str, + is_duplicate: bool, +) -> bool { let directives = parsed_directives(policy); let Some(sources) = directive_source_list(&directives, TRUSTED_TYPES) else { return true; }; - sources.iter().any(|source| { + let name_is_allowed = sources.iter().any(|source| { let source = source.trim(); source == "*" || (!source.is_empty() && !csp_keyword_eq(source, "none") && !csp_keyword_eq(source, "allow-duplicates") && source == policy_name) - }) + }); + let duplicate_is_allowed = !is_duplicate + || sources + .iter() + .any(|source| csp_keyword_eq(source.trim(), "allow-duplicates")); + name_is_allowed && duplicate_is_allowed } fn policy_sandboxes_document_domain(policy: &str) -> bool { @@ -1212,10 +1247,14 @@ fn sandbox_sources_allow_popups_to_escape(sources: &[&str]) -> bool { } fn trusted_types_sink_violation_sample(sink: &str, sample: &str) -> String { - let clipped = sample.chars().take(40).collect::(); + let clipped = trusted_types_violation_sample(sample); format!("{sink}|{clipped}") } +fn trusted_types_violation_sample(sample: &str) -> String { + sample.chars().take(40).collect() +} + fn effective_source_list_with_directive( policy: &str, kind: ContentSecurityPolicyResourceKind, @@ -3299,27 +3338,38 @@ mod tests { #[test] fn trusted_types_directive_filters_policy_names() { - let allowed = |policies: &[&str], name: &str| { - content_security_policy_allows_trusted_type_policy_name( - &policies - .iter() - .map(|policy| policy.to_string()) - .collect::>(), - name, - ) + let allowed = |policies: &[&str], name: &str, is_duplicate: bool| { + policies + .iter() + .all(|policy| policy_allows_trusted_type_policy_name(policy, name, is_duplicate)) }; - assert!(allowed(&[], "SomeName")); - assert!(allowed(&["default-src 'none'"], "SomeName")); - assert!(allowed(&["trusted-types SomeName OtherName"], "SomeName")); - assert!(allowed(&["trusted-types * 'aLLow-dUPLIcates'"], "SomeName")); - assert!(allowed(&["trusted-types 'none' SomeName"], "SomeName")); - assert!(!allowed(&["trusted-types"], "SomeName")); - assert!(!allowed(&["trusted-types 'nONe'"], "SomeName")); - assert!(!allowed(&["trusted-types SomeName"], "default")); + assert!(allowed(&[], "SomeName", false)); + assert!(allowed(&[], "SomeName", true)); + assert!(allowed(&["default-src 'none'"], "SomeName", false)); + assert!(allowed( + &["trusted-types SomeName OtherName"], + "SomeName", + false + )); + assert!(allowed( + &["trusted-types * 'aLLow-dUPLIcates'"], + "SomeName", + true + )); + assert!(allowed( + &["trusted-types 'none' SomeName"], + "SomeName", + false + )); + assert!(!allowed(&["trusted-types"], "SomeName", false)); + assert!(!allowed(&["trusted-types 'nONe'"], "SomeName", false)); + assert!(!allowed(&["trusted-types SomeName"], "SomeName", true)); + assert!(!allowed(&["trusted-types SomeName"], "default", false)); assert!(!allowed( &["trusted-types SomeName", "trusted-types OtherName"], - "SomeName" + "SomeName", + false )); } diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index 68ab68a3cb..58dc57690d 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -31,6 +31,7 @@ const TRUSTED_TYPES_CREATE_HTML_SLOT: &str = "__moliTrustedTypesCreateHTML"; const TRUSTED_TYPES_CREATE_SCRIPT_SLOT: &str = "__moliTrustedTypesCreateScript"; const TRUSTED_TYPES_CREATE_SCRIPT_URL_SLOT: &str = "__moliTrustedTypesCreateScriptURL"; const TRUSTED_TYPES_POLICY_NAME_SLOT: &str = "__moliTrustedTypesPolicyName"; +const TRUSTED_TYPES_CREATED_POLICY_NAMES_SLOT: &str = "__moliTrustedTypesCreatedPolicyNames"; const TRUSTED_TYPES_EMPTY_HTML_SLOT: &str = "__moliTrustedTypesEmptyHTML"; const TRUSTED_TYPES_EMPTY_SCRIPT_SLOT: &str = "__moliTrustedTypesEmptyScript"; const TRUSTED_TYPES_EMPTY_VALUE_SLOTS: [&str; 2] = [ @@ -102,6 +103,8 @@ struct TrustedTypesFactoryObjectDeclaration<'scope> { empty_html: v8::Local<'scope, v8::Object>, #[webapi(slot = TRUSTED_TYPES_EMPTY_SCRIPT_SLOT)] empty_script: v8::Local<'scope, v8::Object>, + #[webapi(slot = TRUSTED_TYPES_CREATED_POLICY_NAMES_SLOT)] + created_policy_names: v8::Local<'scope, v8::Array>, } #[derive(WebApiObject)] @@ -861,7 +864,8 @@ fn trusted_types_create_policy_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + let factory = args.this(); + if !trusted_types_factory_receiver_is_valid(scope, factory) { return; } let Some(name) = webidl::required_argument::( @@ -878,13 +882,21 @@ fn trusted_types_create_policy_callback<'s>( return; }; - if !trusted_types_policy_name_allowed(scope, &name) { + let is_duplicate = trusted_types_policy_name_was_created(scope, factory, &name); + if !trusted_types_policy_name_allowed(scope, &name, is_duplicate) { throw_type_error( scope, "Failed to execute 'createPolicy' on 'TrustedTypePolicyFactory': Content Security Policy disallows creating a policy with the given name.", ); return; } + if name == "default" && is_duplicate { + throw_type_error( + scope, + "Failed to execute 'createPolicy' on 'TrustedTypePolicyFactory': Policy with name \"default\" already exists.", + ); + return; + } let policy_name = v8_string(scope, &name).unwrap_or_else(|| v8::String::empty(scope)); let policy = TrustedTypePolicyObjectDeclaration { @@ -905,17 +917,62 @@ fn trusted_types_create_policy_callback<'s>( policy.into(), ); } + trusted_types_record_created_policy_name(scope, factory, &name, is_duplicate); rv.set(policy.into()); } -fn trusted_types_policy_name_allowed(scope: &mut v8::PinScope<'_, '_>, name: &str) -> bool { - if let Some(allowed) = crate::worker::worker_allows_trusted_type_policy_name(scope, name) { +fn trusted_types_policy_name_was_created<'s>( + scope: &mut v8::PinScope<'s, '_>, + factory: v8::Local<'s, v8::Object>, + name: &str, +) -> bool { + let Some(names) = get_private_value(scope, factory, TRUSTED_TYPES_CREATED_POLICY_NAMES_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return false; + }; + (0..names.length()).any(|index| { + names + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + .is_some_and(|value| value.to_rust_string_lossy(scope) == name) + }) +} + +fn trusted_types_record_created_policy_name<'s>( + scope: &mut v8::PinScope<'s, '_>, + factory: v8::Local<'s, v8::Object>, + name: &str, + was_duplicate: bool, +) { + if was_duplicate { + return; + } + let Some(names) = get_private_value(scope, factory, TRUSTED_TYPES_CREATED_POLICY_NAMES_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(name) = v8_string(scope, name) else { + return; + }; + let _ = names.set_index(scope, names.length(), name.into()); +} + +fn trusted_types_policy_name_allowed( + scope: &mut v8::PinScope<'_, '_>, + name: &str, + is_duplicate: bool, +) -> bool { + if let Some(allowed) = + crate::worker::worker_allows_trusted_type_policy_name_by_csp(scope, name, is_duplicate) + { return allowed; } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return true; }; - unsafe { &*host_ptr }.allows_trusted_type_policy_name(scope, name) + unsafe { &mut *host_ptr }.allows_trusted_type_policy_name_by_csp(scope, name, is_duplicate) } fn trusted_type_policy_name_getter_callback<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs index 2e65681dc0..29b3e7e3c3 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs @@ -207,6 +207,7 @@ fn build_and_cache_trusted_types_state<'s>( let factory = TrustedTypesFactoryObjectDeclaration { empty_html, empty_script, + created_policy_names: v8::Array::new(scope, 0), } .bind(scope) .map_err(|error| anyhow!("failed to bind TrustedTypePolicyFactory object: {error}"))?; diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index e25f451fdb..448c7ba489 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -6,7 +6,6 @@ use crate::content_security_policy::{ ContentSecurityPolicyResourceKind, ContentSecurityPolicyScriptElementRequest, ContentSecurityPolicyStyleElementRequest, ContentSecurityPolicyUrlViolation, ContentSecurityPolicyViolationEventFields, TrustedTypesForScriptRequirements, - content_security_policy_allows_trusted_type_policy_name, content_security_policy_allows_trusted_types_eval, content_security_policy_frame_ancestors_violation_with_disposition_and_reporting_endpoints, content_security_policy_headers, @@ -18,6 +17,7 @@ use crate::content_security_policy::{ content_security_policy_requires_trusted_types_for_script, content_security_policy_script_element_url_violation_with_redirect_status_disposition_reporting_endpoints_and_request, content_security_policy_style_element_url_violation_with_redirect_status_disposition_reporting_endpoints_and_request, + content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints, content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints, content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints, create_security_policy_violation_event, @@ -1450,19 +1450,43 @@ impl DocumentRuntime { document_policies_allow_trusted_types_eval(&policies) } - pub(crate) fn allows_trusted_type_policy_name_for_document( + #[allow(clippy::too_many_arguments)] + pub(crate) fn trusted_type_policy_name_csp_check_for_document( &self, document_handle: Option, + document_url: &Url, response_policies: &[String], + response_report_only_policies: &[String], response_reporting_endpoints: &ContentSecurityPolicyReportingEndpoints, policy_name: &str, - ) -> bool { - let policies = self.document_content_security_policy_strings_for_optional_document( - document_handle, - response_policies, + is_duplicate: bool, + ) -> DocumentContentSecurityPolicyCheck { + let enforced_policies = self + .document_content_security_policy_strings_for_optional_document( + document_handle, + response_policies, + response_reporting_endpoints, + ); + let report_only_policies = document_response_content_security_policy_strings( + response_report_only_policies, response_reporting_endpoints, ); - document_policies_allow_trusted_type_policy_name(policies, policy_name) + 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, + ), + } } pub(crate) fn queue_content_security_policy_violation_event_best_effort<'s>( @@ -2063,12 +2087,26 @@ fn iter_document_trusted_types_sink_policy_violations<'a>( }) } -fn document_policies_allow_trusted_type_policy_name( +fn document_trusted_types_policy_violation_from_document_policies( policies: Vec, + document_url: &Url, policy_name: &str, -) -> bool { - policies.into_iter().all(|policy| { - content_security_policy_allows_trusted_type_policy_name(&[policy.policy], policy_name) + is_duplicate: bool, + disposition: ContentSecurityPolicyDisposition, +) -> Option { + policies.into_iter().find_map(|policy| { + let single_policy = [policy.policy.clone()]; + let mut violation = + content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( + &single_policy, + document_url, + policy_name, + is_duplicate, + disposition, + &policy.reporting_endpoints, + )?; + apply_document_policy_reporting_flags(&mut violation, &policy); + Some(violation) }) } 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 ddd9f4e24d..e44ac221ee 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 @@ -868,25 +868,63 @@ impl JsContextHost { .1 } - pub(crate) fn allows_trusted_type_policy_name( - &self, - scope: &mut v8::PinScope<'_, '_>, + pub(crate) fn allows_trusted_type_policy_name_by_csp<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, policy_name: &str, + is_duplicate: bool, ) -> bool { - let Some(snapshot) = - self.owner_document_policy_snapshot(policy_owner_dispatch_scope(scope)) - else { + let owner = policy_owner_dispatch_scope(scope); + let Some(snapshot) = self.owner_document_policy_snapshot(owner) else { + // A context between documents has no policy owner to report + // against; applying another document's CSP would enforce the + // wrong policy. return true; }; // SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime. - unsafe { &*self.runtime }.allows_trusted_type_policy_name_for_document( + 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(); + 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() + { + 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(); + // 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 { + self.dispatch_content_security_policy_violation_event_for_owner_best_effort( + scope, host_ptr, owner, &violation, + ); + } + allowed } pub(crate) fn requires_trusted_types_for_script( @@ -1010,26 +1048,8 @@ impl JsContextHost { capture_current_script_location: bool, ) { let source_location = (capture_current_script_location && !self.active_inspector_dispatch) - .then(|| v8::StackTrace::current_stack_trace(scope, 1)) - .flatten() - .and_then(|stack| stack.get_frame(scope, 0)) - .map(|frame| { - let source_file = frame - .get_script_name_or_source_url(scope) - .map(|source| source.to_rust_string_lossy(scope)) - .map(|source| { - crate::content_security_policy::content_security_policy_source_file_for_report( - &source, - ) - }) - .unwrap_or_default(); - let line_number = i32::try_from(frame.get_line_number()) - .unwrap_or_default() - .max(0); - let column_number = - i32::try_from(frame.get_column()).unwrap_or_default().max(0); - (source_file, line_number, column_number) - }); + .then(|| current_script_violation_location(scope)) + .flatten(); let owner = policy_owner_dispatch_scope(scope); for mut violation in self.trusted_types_sink_csp_violations_for_owner(owner, sink, sample) { if let Some((source_file, line_number, column_number)) = &source_location { @@ -1275,3 +1295,22 @@ fn current_script_call_location(scope: &v8::PinScope<'_, '_>) -> (i32, i32) { let column = i32::try_from(frame.get_column()).unwrap_or(0).max(0); (line, column) } + +fn current_script_violation_location( + scope: &mut v8::PinScope<'_, '_>, +) -> Option<(String, i32, i32)> { + let stack = v8::StackTrace::current_stack_trace(scope, 1)?; + let frame = stack.get_frame(scope, 0)?; + let source_file = frame + .get_script_name_or_source_url(scope) + .map(|source| source.to_rust_string_lossy(scope)) + .map(|source| { + crate::content_security_policy::content_security_policy_source_file_for_report(&source) + }) + .unwrap_or_default(); + let line_number = i32::try_from(frame.get_line_number()) + .unwrap_or_default() + .max(0); + let column_number = i32::try_from(frame.get_column()).unwrap_or_default().max(0); + Some((source_file, line_number, column_number)) +} 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 3eca38caec..67fe52e396 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 @@ -216,6 +216,72 @@ fn trusted_type_factory_default_policy_getter_tracks_the_branded_policy() { ); } +#[test] +fn trusted_type_policy_creation_reports_name_and_duplicate_csp_violations() { + let mut vm = new_storage_test_vm("https://trusted-type-policy-csp.test/"); + vm.set_response_content_security_policies(&[ + "trusted-types allowed reportOnly duplicate".to_owned() + ]); + vm.set_response_content_security_report_only_policies(&[ + "trusted-types allowed duplicate".to_owned() + ]); + + let result = vm + .eval( + r#" +(() => { + const violations = []; + document.addEventListener("securitypolicyviolation", event => { + violations.push({ + blockedURI: event.blockedURI, + effectiveDirective: event.effectiveDirective, + originalPolicy: event.originalPolicy, + disposition: event.disposition, + sample: event.sample + }); + }); + globalThis.__trustedTypePolicyCspViolations = violations; + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + + const allowed = trustedTypes.createPolicy("allowed", {}); + const reportOnly = trustedTypes.createPolicy("reportOnly", {}); + const duplicate = trustedTypes.createPolicy("duplicate", {}); + const duplicateError = errorName(() => trustedTypes.createPolicy("duplicate", {})); + const blockedError = errorName(() => trustedTypes.createPolicy("blocked", {})); + + return JSON.stringify({ + names: [allowed.name, reportOnly.name, duplicate.name], + duplicateError, + blockedError, + violations + }); +})() +"#, + ) + .expect("TrustedTypePolicy CSP creation probe should evaluate"); + + assert_eq!( + result, + r#"{"names":["allowed","reportOnly","duplicate"],"duplicateError":"TypeError","blockedError":"TypeError","violations":[]}"# + ); + assert_eq!( + drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm), + 5 + ); + assert_eq!( + vm.eval("JSON.stringify(globalThis.__trustedTypePolicyCspViolations)") + .expect("queued TrustedTypePolicy CSP violations should be observable"), + r#"[{"blockedURI":"trusted-types-policy","effectiveDirective":"trusted-types","originalPolicy":"trusted-types allowed duplicate","disposition":"report","sample":"reportOnly"},{"blockedURI":"trusted-types-policy","effectiveDirective":"trusted-types","originalPolicy":"trusted-types allowed reportOnly duplicate","disposition":"enforce","sample":"duplicate"},{"blockedURI":"trusted-types-policy","effectiveDirective":"trusted-types","originalPolicy":"trusted-types allowed duplicate","disposition":"report","sample":"duplicate"},{"blockedURI":"trusted-types-policy","effectiveDirective":"trusted-types","originalPolicy":"trusted-types allowed reportOnly duplicate","disposition":"enforce","sample":"blocked"},{"blockedURI":"trusted-types-policy","effectiveDirective":"trusted-types","originalPolicy":"trusted-types allowed duplicate","disposition":"report","sample":"blocked"}]"# + ); +} + #[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 b49e469eeb..3b192e2a5c 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 @@ -8,6 +8,7 @@ use crate::content_security_policy::{ ContentSecurityPolicyDisposition, ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyResourceKind, ContentSecurityPolicyUrlViolation, ContentSecurityPolicyViolationEventFields, content_security_policy_report_requests, + content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints, content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints, content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints, content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints, @@ -107,6 +108,49 @@ pub(super) fn dispatch_worker_trusted_types_sink_violation_event_for_state<'s>( } } +pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + state: &Rc>, + policy_name: &str, + is_duplicate: bool, +) -> bool { + let (report_only_violation, enforced_violation) = { + 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) + }; + let allowed = enforced_violation.is_none(); + // 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); + } + allowed +} + 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/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 41cefe9d8f..2f6cf155df 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -6160,14 +6160,18 @@ pub(crate) fn worker_current_script_url(scope: &mut v8::PinScope<'_, '_>) -> Opt get_worker_state(scope)?.borrow().current_script_url.clone() } -pub(crate) fn worker_allows_trusted_type_policy_name( +pub(crate) fn worker_allows_trusted_type_policy_name_by_csp( scope: &mut v8::PinScope<'_, '_>, name: &str, + is_duplicate: bool, ) -> Option { + let state = get_worker_state(scope)?; Some( - crate::content_security_policy::content_security_policy_allows_trusted_type_policy_name( - &get_worker_state(scope)?.borrow().content_security_policies, + content_security_policy::allows_worker_trusted_type_policy_name_for_state( + scope, + &state, name, + is_duplicate, ), ) } diff --git a/moli-renderer-v8/src/worker/mod.rs b/moli-renderer-v8/src/worker/mod.rs index 1c92bc176f..856a0ce952 100644 --- a/moli-renderer-v8/src/worker/mod.rs +++ b/moli-renderer-v8/src/worker/mod.rs @@ -37,7 +37,7 @@ pub(crate) use global_scope::{ remove_worker_message_port_event_listener_by_id, reserve_nested_worker_context, send_worker_websocket_binary, send_worker_websocket_text, service_worker_runtime_identity, try_worker_xhr_abort_callback, try_worker_xhr_reschedule_timeout_after_timeout_change, - try_worker_xhr_send_callback, worker_allows_trusted_type_policy_name, + try_worker_xhr_send_callback, worker_allows_trusted_type_policy_name_by_csp, worker_allows_trusted_types_eval, worker_broadcast_channel_registry, worker_broadcast_channel_storage_key, worker_broadcast_channel_wake_sender, worker_broadcast_channel_wrapper, worker_current_script_url, worker_global_is_closed,