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.
This commit is contained in:
ldm0
2026-09-27 07:59:16 +08:00
parent c94d1c4d64
commit 2e290b0be9
12 changed files with 621 additions and 216 deletions
+40 -50
View File
@@ -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<ContentSecurityPolicyUrlViolation> {
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<ContentSecurityPolicyUrlViolation> {
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,
})
}
@@ -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<DomHandle>,
document_url: &Url,
@@ -1475,7 +1475,7 @@ impl DocumentRuntime {
response_reporting_endpoints: &ContentSecurityPolicyReportingEndpoints,
policy_name: &str,
is_duplicate: bool,
) -> DocumentContentSecurityPolicyCheck {
) -> Vec<DocumentContentSecurityPolicyViolation> {
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<Item = DocumentContentSecurityPolicyViolation> + '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<DocumentContentSecurityPolicyString>,
document_url: &Url,
policy_name: &str,
document_url: &'a Url,
policy_name: &'a str,
is_duplicate: bool,
disposition: ContentSecurityPolicyDisposition,
) -> Option<DocumentContentSecurityPolicyViolation> {
policies.into_iter().find_map(|policy| {
let single_policy = [policy.policy.clone()];
) -> impl Iterator<Item = DocumentContentSecurityPolicyViolation> + '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#"<!doctype html>
<meta http-equiv="Content-Security-Policy"
content="trusted-types 'none'; report-uri /meta">"#,
);
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::<Vec<_>>()
})
.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("<!doctype html>");
@@ -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,
);
@@ -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::<serde_json::Value>(&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/");
@@ -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<Item = (&str, ContentSecurityPolicyDisposition)> {
// 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<RefCell<WorkerGlobalState>>,
violations: Vec<ContentSecurityPolicyUrlViolation>,
) {
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,
@@ -1294,32 +1294,41 @@ pub(crate) fn check_worker_websocket_csp(
url: &Url,
) -> Option<WorkerWebSocketCspOutcome> {
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>(
@@ -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 {
@@ -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 {
@@ -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(),
@@ -1536,5 +1536,5 @@ mod strict_script_mime;
mod network;
mod postmessage;
mod tls;
mod trusted_types_reporting;
mod xhr_failure;
@@ -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"}]}"#
@@ -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"],
]),
);
}