fix(trusted-types): gate javascript URL navigation

This commit is contained in:
ldm0
2026-09-28 01:46:58 +08:00
parent d929e95590
commit fbfc7ab074
2 changed files with 178 additions and 0 deletions
@@ -1219,6 +1219,69 @@ location.href
.await;
}
#[tokio::test]
async fn javascript_location_navigation_honors_enforced_trusted_types() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-trusted-types.test/start.html").unwrap(),
);
page_vm
.vm_mut()
.set_response_content_security_policies(&[
"require-trusted-types-for 'script'".to_owned(),
]);
let local_executor = page_vm.local_executor.clone();
let observed = local_executor
.run(async move {
page_vm.vm_mut().eval(
r#"
globalThis.__jsUrlRan = false;
globalThis.__jsUrlViolations = [];
document.addEventListener("securitypolicyviolation", event => {
globalThis.__jsUrlViolations.push({
sample: event.sample,
disposition: event.disposition,
});
});
location.href = "javascript:globalThis.__jsUrlRan=true";
"queued"
"#,
)?;
assert!(page_vm.vm().has_pending_location_navigation());
let mut pending_document_lifecycle_turn = None;
let outcome = page_vm
.follow_pending_location_navigation_one_turn_async(
&mut pending_document_lifecycle_turn,
PageVmInitStage::Load,
)
.await?;
assert!(matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
));
assert_eq!(
page_vm
.vm_mut()
.drain_pre_domcontentloaded_content_security_policy_violation_tasks_for_test(),
1
);
page_vm.vm_mut().eval(
"JSON.stringify({ ran: globalThis.__jsUrlRan, violations: globalThis.__jsUrlViolations, href: location.href })",
)
})
.await
.expect("Trusted Types javascript location navigation should settle");
assert_eq!(
observed,
r#"{"ran":false,"violations":[{"sample":"Location href|globalThis.__jsUrlRan=true","disposition":"enforce"}],"href":"https://javascript-location-trusted-types.test/start.html"}"#
);
})
.await;
}
#[tokio::test]
async fn javascript_string_completion_restarts_renderer_lifecycle_on_same_document_token() {
run_page_vm_async_test(async move {
@@ -2093,6 +2093,121 @@ fn report_only_trusted_types_eval_runs_reports_and_applies_the_default_policy()
);
}
#[test]
fn javascript_url_navigation_default_policy_is_silent_and_disposition_aware() {
let policy = "require-trusted-types-for 'script'".to_owned();
let mut rewritten = new_storage_test_vm("https://javascript-url-rewritten.test/");
rewritten.set_response_content_security_policies(std::slice::from_ref(&policy));
rewritten
.eval(
r#"
globalThis.__javascriptUrlOriginal = 0;
globalThis.__javascriptUrlModified = 0;
globalThis.__javascriptUrlDefaultCalls = [];
trustedTypes.createPolicy("default", {
createScript: (...args) => {
globalThis.__javascriptUrlDefaultCalls.push(args);
return args[0].replace("Original", "Modified");
}
});
"ready"
"#,
)
.expect("install javascript URL rewriting default policy");
assert_eq!(
rewritten
.eval_javascript_url_runtime_turn(
"globalThis.__javascriptUrlOriginal++",
&Url::parse("https://javascript-url-rewritten.test/").expect("test URL"),
)
.expect("rewritten javascript URL should execute"),
None
);
assert_eq!(
rewritten
.eval(
"JSON.stringify({ original: __javascriptUrlOriginal, modified: __javascriptUrlModified, calls: __javascriptUrlDefaultCalls })"
)
.expect("read rewritten javascript URL result"),
r#"{"original":0,"modified":1,"calls":[["globalThis.__javascriptUrlOriginal++","TrustedScript","Location href"]]}"#
);
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut rewritten),
0
);
for (name, callback, report_only, expected_runs, expected_disposition) in [
(
"throw-enforce",
"throw new RangeError('default policy failure')",
false,
0,
"enforce",
),
(
"invalid-enforce",
"return '//make:invalid/'",
false,
0,
"enforce",
),
(
"throw-report",
"throw new RangeError('default policy failure')",
true,
1,
"report",
),
] {
let base_url =
Url::parse(&format!("https://javascript-url-{name}.test/")).expect("test URL");
let mut vm = new_storage_test_vm(base_url.as_str());
if report_only {
vm.set_response_content_security_report_only_policies(std::slice::from_ref(&policy));
} else {
vm.set_response_content_security_policies(std::slice::from_ref(&policy));
}
vm.eval(&format!(
r#"
globalThis.__javascriptUrlRuns = 0;
globalThis.__javascriptUrlViolations = [];
document.addEventListener("securitypolicyviolation", event => {{
globalThis.__javascriptUrlViolations.push({{
sample: event.sample,
disposition: event.disposition,
}});
}});
trustedTypes.createPolicy("default", {{
createScript: () => {{ {callback} }}
}});
"ready"
"#
))
.expect("install failing javascript URL default policy");
assert_eq!(
vm.eval_javascript_url_runtime_turn("globalThis.__javascriptUrlRuns++", &base_url)
.expect("default policy failure must not escape the navigation turn"),
None
);
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
1
);
assert_eq!(
vm.eval(
"JSON.stringify({ runs: __javascriptUrlRuns, violations: __javascriptUrlViolations })"
)
.expect("read javascript URL failure result"),
format!(
r#"{{"runs":{expected_runs},"violations":[{{"sample":"Location href|globalThis.__javascriptUrlRuns++","disposition":"{expected_disposition}"}}]}}"#
),
"case {name}"
);
}
}
#[test]
fn report_only_default_policy_transforms_or_preserves_by_callback_outcome() {
let mut vm = new_storage_test_vm("https://default-policy-report-only.test/");