From 577aef69e0e631fe2c7ef783cdca15e8d7550fc2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 17 Sep 2026 16:05:00 +0800 Subject: [PATCH] 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. --- moli-renderer-v8/src/host/timers.rs | 16 ++- moli-renderer-v8/src/script_vm/tests/mod.rs | 2 + .../src/script_vm/tests/string_timers.rs | 99 +++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/string_timers.rs diff --git a/moli-renderer-v8/src/host/timers.rs b/moli-renderer-v8/src/host/timers.rs index fe931ed7cf..6b7f443e25 100644 --- a/moli-renderer-v8/src/host/timers.rs +++ b/moli-renderer-v8/src/host/timers.rs @@ -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, } struct ScheduledTimerFunction { @@ -491,6 +494,9 @@ impl HostTimeoutScheduler { owner: HostTimerOwner, extra_args: Vec>, ) -> 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>, ) -> 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(); diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index b3dc8a8386..3ade5223b5 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -15219,3 +15219,5 @@ mod webidl_receivers; mod webidl_trusted_types; mod websocket; mod window_execution_context; + +mod string_timers; diff --git a/moli-renderer-v8/src/script_vm/tests/string_timers.rs b/moli-renderer-v8/src/script_vm/tests/string_timers.rs new file mode 100644 index 0000000000..4f8aee4120 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/string_timers.rs @@ -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); + } +}