mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
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:
@@ -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"],
|
||||
]),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user