fix(csp): validate Trusted Types directive name tokens

Discard non-ASCII directives independently before duplicate lookup and retain exact ASCII whitespace boundaries. Match literal Trusted Types allowlist entries only when they satisfy tt-policy-name, while preserving arbitrary policy names without CSP or under a wildcard.

Add exhaustive ASCII token tests, directive parsing regressions, and enforce/report-only delivery coverage. Validated with fmt, full-workspace Clippy and all 17,735 nextest tests. The 219-case Trusted Types suite gains one passing case and five passing subtests in both CLI and CDP, with all other semantic results unchanged.
This commit is contained in:
ldm0
2026-09-14 16:14:48 +08:00
parent d4b8947ca9
commit 879cc31834
4 changed files with 98 additions and 6 deletions
@@ -3280,7 +3280,6 @@ svg/types/scripted/SVGLength-viewport.html
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/should-trusted-type-policy-creation-be-blocked-by-csp-002.html
trusted-types/trusted-types-createHTMLDocument.html
trusted-types/trusted-types-report-only.html
trusted-types/trusted-types-reporting-for-DedicatedWorker-ServiceWorkerContainer-register.https.html
@@ -8437,6 +8437,7 @@ trusted-types/should-sink-type-mismatch-violation-be-blocked-by-csp-001.html
trusted-types/should-sink-type-mismatch-violation-be-blocked-by-csp-002-worker.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-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
@@ -1199,6 +1199,9 @@ fn policy_allows_trusted_type_policy_name(
source == "*"
|| (!source.is_empty()
&& source == policy_name
// Only tt-policy-name tokens can whitelist a literal name.
// The policy API itself accepts arbitrary strings, including
// under a wildcard, so do not validate policy_name globally.
&& source.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'-' | b'#' | b'=' | b'_' | b'/' | b'@' | b'.' | b'%')
@@ -1207,7 +1210,7 @@ fn policy_allows_trusted_type_policy_name(
let duplicate_is_allowed = !is_duplicate
|| sources
.iter()
.any(|source| csp_keyword_eq(source.trim(), "allow-duplicates"));
.any(|source| csp_keyword_eq(source, "allow-duplicates"));
name_is_allowed && duplicate_is_allowed
}
@@ -3477,26 +3480,31 @@ mod tests {
assert!(policy_allows_trusted_type_policy_name(
&format!("trusted-types {name}"),
name,
false
false,
));
}
assert!(policy_allows_trusted_type_policy_name(
"trusted-types valid policy*name",
"valid",
false
false,
));
assert!(!policy_allows_trusted_type_policy_name(
"trusted-types valid policy*name",
"policy*name",
false
false,
));
for wildcard in ["\u{000b}*", "*\u{000b}", "policy*"] {
assert!(!policy_allows_trusted_type_policy_name(
&format!("trusted-types {wildcard}"),
"valid",
false
false,
));
}
assert!(!policy_allows_trusted_type_policy_name(
"trusted-types valid \u{000b}'allow-duplicates'",
"valid",
true,
));
}
#[test]
@@ -3512,6 +3520,16 @@ mod tests {
for policy in ["", "trusted-types *"] {
assert!(policy_allows_trusted_type_policy_name(policy, name, false));
}
assert!(!policy_allows_trusted_type_policy_name(
"trusted-types *",
name,
true,
));
assert!(policy_allows_trusted_type_policy_name(
"trusted-types * 'allow-duplicates'",
name,
true,
));
}
}
@@ -433,6 +433,80 @@ fn trusted_type_policy_creation_reports_name_and_duplicate_csp_violations() {
);
}
#[test]
fn trusted_types_csp_name_grammar_preserves_enforcement_and_report_only_delivery() {
let policy = "trusted-types valid policy*name policy$name policy?name policy!name";
let names = [
"valid",
"policy*name",
"policy$name",
"policy?name",
"policy!name",
"política",
"",
];
for enforce in [false, true] {
let mut vm = new_storage_test_vm("https://trusted-types-name-grammar.test/");
if enforce {
vm.set_response_content_security_policies(&[
policy.to_owned(),
"trusted-types ignored política".to_owned(),
]);
}
vm.set_response_content_security_report_only_policies(&[
policy.to_owned(),
"trusted-types ignored política".to_owned(),
]);
let actual = vm.eval(r#"
globalThis.policyNameGrammarReports = [];
document.addEventListener('securitypolicyviolation', event => {
policyNameGrammarReports.push([event.sample, event.originalPolicy, event.disposition]);
});
JSON.stringify(['valid', 'policy*name', 'policy$name', 'policy?name', 'policy!name', 'política', ''].map(name => {
try { return trustedTypes.createPolicy(name).name; }
catch (error) { return error instanceof TypeError ? 'TypeError' : error.name; }
}));
"#).unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(&actual).unwrap(),
if enforce {
serde_json::json!([
"valid",
"TypeError",
"TypeError",
"TypeError",
"TypeError",
"TypeError",
"TypeError"
])
} else {
serde_json::json!(names)
},
);
assert_eq!(vm.eval("policyNameGrammarReports.length").unwrap(), "0");
let expected = names[1..]
.iter()
.flat_map(|name| {
let mut reports = Vec::new();
if enforce {
reports.push(serde_json::json!([name, policy, "enforce"]));
}
reports.push(serde_json::json!([name, policy, "report"]));
reports
})
.collect::<Vec<_>>();
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
expected.len(),
);
let actual = vm.eval("JSON.stringify(policyNameGrammarReports)").unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(&actual).unwrap(),
serde_json::json!(expected),
);
}
}
#[test]
fn trusted_type_policy_creation_reports_every_violated_policy() {
let enforced = [