fix(csp): preserve policy ownership across document input stream resets

This commit is contained in:
ldm0
2026-11-03 18:08:16 +08:00
parent 30a66c2345
commit 29cb9a6739
7 changed files with 140 additions and 11 deletions
@@ -326,13 +326,7 @@ impl DocumentRuntime {
self.clear_classic_defer_timer_schedule_ranges();
self.pending_parser_blocking_work = None;
self.root_document_parser = None;
self.delivered_meta_content_security_policies
.get_mut()
.clear();
self.local_worker_policy_sources.get_mut().clear();
self.processed_meta_content_security_policy_handles
.get_mut()
.clear();
self.reset_meta_content_security_policy_processing_for_document(self.document_handle());
self.document_input_stream_opened = true;
self.set_document_ready_state(DocumentReadyState::Loading);
}
@@ -2401,6 +2401,18 @@ pub(crate) fn set_response_content_security_report_only_policies(
.unwrap_or_default()
}
pub(crate) fn reset_meta_content_security_policy_processing_for_document(
&self,
document: DomHandle,
) {
// document.open() replaces the input stream of this same Document.
// Its installed policies and live blob/worker policy source survive;
// meta elements inserted into the new stream are processed again.
self.processed_meta_content_security_policy_handles
.borrow_mut()
.retain(|(owner, _)| *owner != document);
}
pub(crate) fn initialize_inherited_meta_content_security_policies(
&self,
document_handle: DomHandle,
@@ -3699,6 +3711,17 @@ mod tests {
.is_enforced(),
"checking the parent must not discard the child document's delivered policy"
);
runtime.open_document();
assert!(
runtime
.dom_host_mut()
.set_attribute(child_meta, "content", "script-src 'none'")
);
assert_eq!(
runtime.meta_content_security_policy_strings_for_document(child_document),
["require-trusted-types-for 'script'"],
"reopening the parent must not discard or reprocess another Document's delivered meta"
);
}
#[test]
@@ -3783,7 +3806,7 @@ mod tests {
}
#[test]
fn document_open_does_not_reuse_replaced_document_meta_csp() {
fn document_open_preserves_the_same_documents_delivered_meta_csp() {
let mut runtime = runtime_for_html(
r#"<!doctype html>
<meta http-equiv="Content-Security-Policy"
@@ -3791,12 +3814,14 @@ mod tests {
"#,
);
assert!(runtime.requires_trusted_types_for_script());
let document = runtime.document_handle();
runtime.open_document();
assert_eq!(runtime.document_handle(), document);
assert!(
!runtime.requires_trusted_types_for_script(),
"the replacement document must not inherit delivered meta policies"
runtime.requires_trusted_types_for_script(),
"reopening the same Document's input stream must retain its installed policy"
);
}
@@ -721,6 +721,7 @@ impl JsContextHost {
// document.open() reuses this Document and its mutable origin.
entry.clear_script_execution_state();
}
host.reset_meta_content_security_policy_processing_for_document(document_handle);
host.request_child_frame_realm_materialization(child_handle);
host.install_empty_child_classic_script_runner_for_current_document(
child_handle,
@@ -196,6 +196,7 @@ impl JsContextHost {
// The input stream changes, but the same Document retains its
// autofocus processed flag. Retire only its old candidates.
host.clear_autofocus_candidates(document_handle);
host.reset_meta_content_security_policy_processing_for_document(document_handle);
host.set_lightweight_popup_same_document_url(scope, popup_id, url.clone());
host.dom_host_mut()
.set_html_quirks_mode_for_parser_document(
@@ -66,7 +66,10 @@ fn policy_owner_dispatch_scope(scope: &mut v8::PinScope<'_, '_>) -> OwnerDispatc
if let Some(popup_id) = active_lightweight_popup_id(scope) {
return OwnerDispatchScope::LightweightPopup(popup_id);
}
OwnerDispatchScope::Top
// A borrowed popup intrinsic enters its realm without an ambient popup
// task marker. Its Document still owns code generation and Trusted Types.
let global = scope.get_current_context().global(scope);
policy_owner_dispatch_scope_for_global(scope, global)
}
fn policy_owner_dispatch_scope_for_global<'s>(
@@ -524,6 +524,68 @@ async fn shared_worker_error_commits_child_navigation_before_document_script_rea
.await;
}
#[tokio::test]
async fn blob_worker_keeps_its_creator_policy_source_across_document_open() {
run_page_vm_async_test(async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(
&loader,
Url::parse("https://document-open-blob-policy.test/page.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
local_executor
.run(async move {
page_vm
.vm_mut()
.document_runtime
.dom_host_mut()
.reset_html_document_shell();
page_vm.vm_mut().eval(
r#"
globalThis.__documentOpenWorkerResult = null;
const meta = document.createElement('meta');
meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute('content', "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:");
document.head.appendChild(meta);
const workerUrl = URL.createObjectURL(new Blob([
'try { eval("0"); postMessage("allowed"); } catch (error) { postMessage(error.name); }'
], {type: 'text/javascript'}));
document.open(); document.close();
const added = document.createElement('meta');
added.setAttribute('http-equiv', 'Content-Security-Policy');
added.setAttribute('content', "script-src 'self' 'unsafe-inline' blob:");
document.head.appendChild(added);
const worker = new Worker(workerUrl);
worker.onmessage = event => {
globalThis.__documentOpenWorkerResult = event.data;
worker.terminate(); URL.revokeObjectURL(workerUrl);
};
worker.onerror = event => {
event.preventDefault();
globalThis.__documentOpenWorkerResult = 'worker-error:' + event.message;
worker.terminate(); URL.revokeObjectURL(workerUrl);
};
"#,
)?;
drive_websocket_until_done(
&mut page_vm,
"String(globalThis.__documentOpenWorkerResult !== null)",
"blob worker must observe the same Document's later meta policy",
)
.await?;
assert_eq!(
page_vm.vm_mut().eval("globalThis.__documentOpenWorkerResult")?,
"EvalError",
);
anyhow::Ok(())
})
.await
.expect("document-open blob worker CSP regression must finish on its owner lane");
})
.await;
}
#[tokio::test]
async fn blob_worker_created_in_data_iframe_inherits_opaque_broadcast_channel_owner_key() {
run_page_vm_async_test(async move {
@@ -4,6 +4,49 @@ mod iframe_navigation;
mod inline_tasks;
mod parser_positions;
#[test]
fn document_open_keeps_policy_and_processes_meta_in_the_new_stream() {
for owner in ["window", "frame.contentWindow", "popup"] {
let mut vm = new_storage_test_vm("https://document-open-policy.test/");
vm.document_runtime
.dom_host_mut()
.reset_html_document_shell();
assert_eq!(
vm.eval(&format!(
r#"(() => {{
const frame = document.createElement('iframe');
document.body.appendChild(frame);
const popup = open();
const owner = {owner};
const doc = owner.document;
const meta = doc.createElement('meta');
meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute('content', "script-src 'self' 'unsafe-inline' 'unsafe-eval'");
doc.head.appendChild(meta);
const evaluate = () => {{
try {{ return owner.eval('40 + 2'); }}
catch (error) {{ return error.name + ':' + (error instanceof owner.EvalError); }}
}};
const before = evaluate();
doc.open(); doc.close();
const after = evaluate();
meta.setAttribute('content', "script-src 'self' 'unsafe-inline'");
doc.head.appendChild(meta);
const tightened = evaluate();
doc.open(); doc.close();
const retained = evaluate();
const sameDocument = doc === owner.document;
popup.close(); frame.remove();
return JSON.stringify([before, after, tightened, retained, sameDocument]);
}})()"#
))
.unwrap(),
r#"[42,42,"EvalError:true","EvalError:true",true]"#,
"{owner}: document.open must retain installed CSP while processing reinserted meta in the new stream",
);
}
}
#[test]
fn inline_script_and_handler_csp_accept_base64url_hashes() {
for hash in [