fix(timers): retain the initiating script nonce

Snapshot the initiating script nonce when scheduling string timeouts and intervals, then restore it on the compiled timer source. Cover dynamic imports, nested timers, identical source scheduled under different nonces, and functions invoked by another script.
This commit is contained in:
ldm0
2026-09-18 00:34:11 +08:00
committed by Donough Liu
parent f082f8a95b
commit 577aef69e0
3 changed files with 115 additions and 2 deletions
+14 -2
View File
@@ -18,7 +18,8 @@ use crate::{
page_task_queue::RendererPageTimerSelection,
script_provenance::CompiledStringProvenance,
util::{
context_host_ptr_from_global_bridge, create_script_origin_with_base_url, get_private_value,
context_host_ptr_from_global_bridge, create_script_origin_with_base_url_and_nonce,
get_private_value, script_nonce_from_host_defined_options,
},
};
use moli_time::{TimerId, TimerReadyAllowance, TimerScheduler};
@@ -42,6 +43,8 @@ struct ScheduledTimerSource {
use_target_context: bool,
source: String,
provenance: CompiledStringProvenance,
// A snapshot of the initiating script, not the eventual timer caller/realm.
script_nonce: Option<String>,
}
struct ScheduledTimerFunction {
@@ -491,6 +494,9 @@ impl HostTimeoutScheduler {
owner: HostTimerOwner,
extra_args: Vec<v8::Global<v8::Value>>,
) -> u32 {
let script_nonce = scope
.get_current_host_defined_options()
.and_then(|options| script_nonce_from_host_defined_options(scope, options));
let Some(owner) = scheduled_timer_owner_for_target(scope, owner, Some(receiver), context)
else {
return 0;
@@ -517,6 +523,7 @@ impl HostTimeoutScheduler {
use_target_context,
source,
provenance,
script_nonce,
}),
owner,
is_interval: false,
@@ -539,6 +546,9 @@ impl HostTimeoutScheduler {
owner: HostTimerOwner,
extra_args: Vec<v8::Global<v8::Value>>,
) -> u32 {
let script_nonce = scope
.get_current_host_defined_options()
.and_then(|options| script_nonce_from_host_defined_options(scope, options));
let Some(owner) = scheduled_timer_owner_for_target(scope, owner, Some(receiver), context)
else {
return 0;
@@ -565,6 +575,7 @@ impl HostTimeoutScheduler {
use_target_context,
source,
provenance,
script_nonce,
}),
owner,
is_interval: true,
@@ -1235,11 +1246,12 @@ fn run_window_timer_source(
exception: None,
}));
};
let origin = create_script_origin_with_base_url(
let origin = create_script_origin_with_base_url_and_nonce(
&mut scope,
source.provenance.source_url().as_str(),
0,
Some(source.provenance.module_base_url()),
source.script_nonce.as_deref(),
);
let Some(script) = v8::Script::compile(&scope, source_value, Some(&origin)) else {
let exception = scope.exception();
@@ -15219,3 +15219,5 @@ mod webidl_receivers;
mod webidl_trusted_types;
mod websocket;
mod window_execution_context;
mod string_timers;
@@ -0,0 +1,99 @@
use super::*;
use crate::script_provenance::CompiledStringProvenance;
fn execute_nonce_script(vm: &mut ScriptVm, source: &str, url: &Url, nonce: Option<&str>) {
vm.exec_in_enclosing_script_turn_with_provenance(
source,
&CompiledStringProvenance::at_url(url.clone()),
0,
nonce,
true,
)
.map_err(|error| error.into_anyhow())
.expect("the initiating script should execute");
}
fn consume_string_timer(vm: &mut ScriptVm) {
assert!(matches!(
vm.run_next_timeout_for_test()
.expect("string timer should execute"),
crate::host::HostTimeoutRunResult::Consumed
));
}
fn assert_import_nonce(vm: &mut ScriptVm, base_url: &Url, nonce: Option<&str>) {
let request = vm
.document_runtime
.take_next_native_dynamic_module_import()
.expect("the timer should queue a dynamic import")
.into_dynamic_import_request();
assert_eq!(request.specifier(), "./dependency.mjs");
assert_eq!(request.base_url(), base_url);
assert_eq!(request.fetch_metadata().nonce.as_deref(), nonce);
assert!(!request.fetch_metadata().parser_inserted);
assert!(request.fetch_metadata().integrity.is_none());
}
#[test]
fn string_timers_preserve_nonce_for_timeout_interval_and_nested_source() {
for (source, timer_turns) in [
(r#"setTimeout("import('./dependency.mjs')", 0);"#, 1),
(
r#"globalThis.__nonceInterval = setInterval("clearInterval(__nonceInterval); import('./dependency.mjs')", 0);"#,
1,
),
(
r#"setTimeout("setTimeout(\"import('./dependency.mjs')\", 0)", 0);"#,
2,
),
] {
let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html");
let script_url = Url::parse("https://timer-nonce.test/scripts/initiator.js").unwrap();
execute_nonce_script(&mut vm, source, &script_url, Some("initiating-nonce"));
for _ in 0..timer_turns {
consume_string_timer(&mut vm);
}
assert_import_nonce(&mut vm, &script_url, Some("initiating-nonce"));
}
}
#[test]
fn string_timers_keep_nonce_snapshots_separate_for_identical_source_and_url() {
let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html");
let script_url = Url::parse("https://timer-nonce.test/scripts/same.js").unwrap();
let nonces = [Some("first-nonce"), None, Some("last-nonce")];
for nonce in nonces {
execute_nonce_script(
&mut vm,
r#"setTimeout("import('./dependency.mjs')", 0);"#,
&script_url,
nonce,
);
}
for nonce in nonces {
consume_string_timer(&mut vm);
assert_import_nonce(&mut vm, &script_url, nonce);
}
}
#[test]
fn string_timers_use_the_active_function_nonce_without_borrowing_the_callers_nonce() {
for initiating_nonce in [Some("function-nonce"), None] {
let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html");
let script_url = Url::parse("https://timer-nonce.test/scripts/function.js").unwrap();
execute_nonce_script(
&mut vm,
r#"globalThis.scheduleFromInitiator = () => setTimeout("import('./dependency.mjs')", 0);"#,
&script_url,
initiating_nonce,
);
execute_nonce_script(
&mut vm,
"scheduleFromInitiator();",
&Url::parse("https://timer-nonce.test/other/caller.js").unwrap(),
Some("caller-nonce"),
);
consume_string_timer(&mut vm);
assert_import_nonce(&mut vm, &script_url, initiating_nonce);
}
}