diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 65cfb8c8c..8fa06a0e2 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -538,6 +538,17 @@ fn queue_browser_owned_top_level_history_traversal<'s>( { return; } + let top_owner = super::navigation_window::runtime_top_window_owner(scope, owner); + if let Some(source_handle) = + super::navigation_window::child_browsing_context_handle_for_runtime_owner(scope, owner) + && super::location_navigation::sandbox_blocks_ancestor_or_top_navigation_from_source( + scope, + source_handle, + top_owner, + ) + { + return; + } unsafe { &mut *host_ptr }.record_pending_top_level_history_traversal(delta); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs index e7f83ad15..dc6839e00 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/traversal.rs @@ -74,6 +74,94 @@ fn top_level_navigation_intents_keep_one_last_writer_state() { ); } +#[tokio::test] +async fn browser_owned_history_traversal_checks_active_source_sandbox() { + use moli_session_history::{ + JointSessionHistory, SessionHistoryContextId, SessionHistoryEntry, SessionHistoryPosition, + }; + + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let url = "https://browser-owned-history.test/current"; + let restricted = "allow-scripts allow-same-origin"; + let permitted = "allow-scripts allow-same-origin allow-top-navigation"; + for (initial, changed, allowed) in [ + (None, Some(restricted), true), + (Some(permitted), Some(restricted), true), + (Some(restricted), None, false), + (Some(restricted), Some(permitted), false), + ] { + let mut vm = new_storage_page_task_executor_test_vm_with_loader(url, &loader); + let mut seed = moli_page_types::initial_navigation_history_seed(false, url); + let mut traversable = JointSessionHistory::new(SessionHistoryPosition::new(1, 3).unwrap()); + traversable.attach( + SessionHistoryContextId::ROOT, + None, + SessionHistoryEntry { + key: seed.entries[0].key.clone(), + document: seed.entries[0].document_id.clone(), + }, + ); + seed.session_history.traversable = Some(Box::new(traversable)); + vm.install_navigation_bootstrap_entry(Some(seed)); + let initial = serde_json::to_string(&initial).unwrap(); + let changed = serde_json::to_string(&changed).unwrap(); + vm.eval(&format!( + r#" +globalThis.frame = document.createElement('iframe'); +if ({initial} !== null) frame.setAttribute('sandbox', {initial}); +(document.body || document.documentElement || document).appendChild(frame); +frame.contentWindow; +"# + )) + .unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + vm.eval(&format!( + "if ({changed} === null) frame.removeAttribute('sandbox'); else frame.setAttribute('sandbox', {changed});" + )) + .unwrap(); + { + let mut host = vm._context_host.borrow_mut(); + let model = host.session_histories.get_mut(None); + // Both destinations exist in browser history but have no renderer ROOT entry. + for delta in [-1, 1] { + let step = model.step_by_delta(delta).unwrap(); + assert!( + !model + .entries_at(step) + .unwrap() + .contains_key(&SessionHistoryContextId::ROOT) + ); + } + } + for (call, delta) in [ + ("back()", -1), + ("forward()", 1), + ("go(-1)", -1), + ("go(1)", 1), + ] { + vm.eval(&format!("frame.contentWindow.history.{call}")) + .unwrap(); + let pending = vm + .take_pending_top_level_history_traversal() + .map(|pending| pending.delta); + assert_eq!( + pending, + allowed.then_some(delta), + "initial={initial}, changed={changed}, call={call}" + ); + } + // A method borrowed from the child still uses its top-level History receiver. + vm.eval("frame.contentWindow.history.back.call(history)") + .unwrap(); + assert_eq!( + vm.take_pending_top_level_history_traversal().unwrap().delta, + -1 + ); + } +} + #[test] fn traversal_factories_apply_webidl_boundaries() { let mut vm = new_parsed_test_vm(