diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index 5c062e9868..289a165180 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -1,3 +1,4 @@ +use super::history_runtime::require_fully_active_history_owner; use super::location_runtime::{location_href_slot, sync_location_object}; use super::navigation_activation::bind_navigation_entry_runtime_owner; use super::navigation_callbacks::cancel_active_intercepted_same_document_navigation; @@ -23,8 +24,7 @@ use super::navigation_result::{ }; use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner; use super::navigation_window::{ - runtime_window_is_global, runtime_window_owner, window_location_for_holder, - window_navigation_for_holder, + runtime_window_is_global, window_location_for_holder, window_navigation_for_holder, }; use super::*; use crate::webidl; @@ -104,11 +104,13 @@ fn mutate_history_object<'s>( return; }; let _ = &parsed.unused; + let Some(owner) = require_fully_active_history_owner(scope, history) else { + return; + }; let Some(state) = structured_clone_value_for_storage(scope, parsed.state) else { return; }; let state_json = stringify_history_state(scope, state); - let owner = runtime_window_owner(scope, history); let Some(location) = window_location_for_holder(scope, owner) else { return; }; diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime.rs index 233f52a088..24957e3626 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime.rs @@ -1,8 +1,10 @@ +mod admission; mod apply; mod length; mod results; mod traversal; +pub(super) use self::admission::require_fully_active_history_owner; pub(super) use self::apply::apply_history_entry; pub(crate) use self::length::{ increment_top_level_history_length_for_runtime_owner, diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs new file mode 100644 index 0000000000..e2c80fa977 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs @@ -0,0 +1,27 @@ +use super::super::navigation_window::{navigation_document_is_active, runtime_window_owner}; +use super::super::{context_host_ptr_from_global_bridge, throw_dom_exception_value}; + +/// History API algorithms call this after WebIDL argument conversion. +pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>( + scope: &mut v8::PinScope<'s, '_>, + history: v8::Local<'s, v8::Object>, +) -> Option> { + let owner = runtime_window_owner(scope, history); + let is_fully_active = if let Some(popup_id) = + crate::native_bridge::lightweight_popup_id_from_window(scope, owner) + { + context_host_ptr_from_global_bridge(scope) + .is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)) + } else { + navigation_document_is_active(scope, owner) + }; + if is_fully_active { + return Some(owner); + } + throw_dom_exception_value( + scope, + "The associated Document is not fully active.", + "SecurityError", + ); + None +} diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs b/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs index 25ea957905..13e558844f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs @@ -1,3 +1,4 @@ +use super::super::history_runtime::require_fully_active_history_owner; use super::super::navigation_activation::{ navigation_activation_value, navigation_current_entry_value, navigation_transition_value, }; @@ -151,6 +152,9 @@ fn history_length_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if require_fully_active_history_owner(scope, args.this()).is_none() { + return; + } let value = history_length_value(scope, args.this()) .unwrap_or_else(|| v8::Number::new(scope, 0.0).into()); rv.set(value); @@ -161,6 +165,9 @@ fn history_state_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if require_fully_active_history_owner(scope, args.this()).is_none() { + return; + } let value = history_state_value(scope, args.this()); rv.set(value); } @@ -212,6 +219,9 @@ fn history_scroll_restoration_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if require_fully_active_history_owner(scope, args.this()).is_none() { + return; + } let value = history_scroll_restoration_value(scope, args.this()) .unwrap_or_else(|| v8str(scope, "auto").into()); rv.set(value); @@ -232,6 +242,9 @@ fn history_scroll_restoration_setter_function<'s>( let Some(next_value) = ScrollRestoration::parse(&next_value) else { return; }; + if require_fully_active_history_owner(scope, args.this()).is_none() { + return; + } set_history_scroll_restoration(scope, args.this(), next_value.label()); } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 49e433396b..c542a4b766 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -1,5 +1,6 @@ use super::history_runtime::{ cancel_pending_precommit_history_traversal, pending_history_traversal_target_index, + require_fully_active_history_owner, }; use super::location_navigation::{ LocationNavigationKind, navigate_location_object, @@ -69,14 +70,43 @@ pub(super) fn history_go_callback<'s>( ); return; } - let delta = if args.length() == 0 { + let action = if args.length() == 0 { Some(HistoryGoAction::Reload) } else { coerce_history_go_delta(scope, args.get(0)) }; - match delta { - Some(HistoryGoAction::Reload) => { - let owner = runtime_window_owner(scope, args.this()); + let Some(action) = action else { + return; + }; + history_go_with_action(scope, args.this(), action); +} + +pub(super) fn history_back_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + history_go_with_action(scope, args.this(), HistoryGoAction::Traverse(-1)); +} + +pub(super) fn history_forward_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + history_go_with_action(scope, args.this(), HistoryGoAction::Traverse(1)); +} + +fn history_go_with_action<'s>( + scope: &mut v8::PinScope<'s, '_>, + history: v8::Local<'s, v8::Object>, + action: HistoryGoAction, +) { + let Some(owner) = require_fully_active_history_owner(scope, history) else { + return; + }; + match action { + HistoryGoAction::Reload => { let Some(location) = window_location_for_holder(scope, owner) else { return; }; @@ -87,28 +117,11 @@ pub(super) fn history_go_callback<'s>( None, ); } - Some(HistoryGoAction::Traverse(delta)) => history_traverse(scope, args.this(), delta), - Some(HistoryGoAction::Noop) => {} - None => {} + HistoryGoAction::Traverse(delta) => history_traverse(scope, history, delta), + HistoryGoAction::Noop => {} } } -pub(super) fn history_back_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - _rv: v8::ReturnValue<'_, v8::Value>, -) { - history_traverse(scope, args.this(), -1); -} - -pub(super) fn history_forward_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - _rv: v8::ReturnValue<'_, v8::Value>, -) { - history_traverse(scope, args.this(), 1); -} - pub(super) fn navigation_back_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index 9a10908a22..997207b334 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -1449,6 +1449,155 @@ fn history_navigation_arguments_use_webidl_conversion() { ); } +#[test] +fn history_operations_reject_a_removed_child_document() { + let mut vm = new_storage_test_vm("https://example.com/page.html"); + + let result = vm + .eval( + r#" + (() => { + const root = document.documentElement || + document.appendChild(document.createElement("html")); + const body = document.body || root.appendChild(document.createElement("body")); + const frame = document.createElement("iframe"); + body.appendChild(frame); + const childHistory = frame.contentWindow.history; + const ChildDOMException = frame.contentWindow.DOMException; + frame.remove(); + + const probe = callback => { + try { + callback(); + return "no throw"; + } catch (error) { + return `${error.name}:${error instanceof ChildDOMException}`; + } + }; + return [ + probe(() => childHistory.length), + probe(() => childHistory.scrollRestoration), + probe(() => childHistory.state), + probe(() => { childHistory.scrollRestoration = "manual"; }), + probe(() => childHistory.go()), + probe(() => childHistory.go(0)), + probe(() => childHistory.go(-1)), + probe(() => childHistory.go(1)), + probe(() => childHistory.go(Infinity)), + probe(() => childHistory.go(-Infinity)), + probe(() => childHistory.back()), + probe(() => childHistory.forward()), + probe(() => childHistory.pushState(1, "", "?x=1")), + probe(() => childHistory.replaceState(2, "", "?x=2")) + ].join("|"); + })() + "#, + ) + .expect("removed child History operations should be rejected"); + + assert_eq!(result, ["SecurityError:true"; 14].join("|")); +} + +#[test] +fn history_argument_errors_precede_inactive_document_errors() { + let mut vm = new_storage_test_vm("https://example.com/page.html"); + + let result = vm + .eval( + r#" + (() => { + const root = document.documentElement || + document.appendChild(document.createElement("html")); + const body = document.body || root.appendChild(document.createElement("body")); + const frame = document.createElement("iframe"); + body.appendChild(frame); + const childHistory = frame.contentWindow.history; + frame.remove(); + + const conversionError = new RangeError("conversion"); + const rejected = { + [Symbol.toPrimitive]() { throw conversionError; } + }; + const probe = callback => { + try { + callback(); + return "no throw"; + } catch (error) { + return error === conversionError ? "conversion" : error.name; + } + }; + return [ + probe(() => childHistory.go(Symbol())), + probe(() => childHistory.go(1n)), + probe(() => childHistory.go(rejected)), + probe(() => { childHistory.scrollRestoration = Symbol(); }), + probe(() => { childHistory.scrollRestoration = rejected; }), + probe(() => { childHistory.scrollRestoration = "invalid"; }), + probe(() => childHistory.pushState()), + probe(() => childHistory.pushState(null, Symbol())), + probe(() => childHistory.replaceState(null, "", Symbol())), + probe(() => childHistory.pushState(null, rejected)), + probe(() => childHistory.replaceState(null, "", rejected)), + probe(() => childHistory.pushState(() => {}, "")), + probe(() => childHistory.replaceState(() => {}, "")) + ].join("|"); + })() + "#, + ) + .expect("inactive History should preserve conversion and serialization error ordering"); + + assert_eq!( + result, + "TypeError|TypeError|conversion|TypeError|conversion|no throw|TypeError|TypeError|TypeError|conversion|conversion|SecurityError|SecurityError" + ); +} + +#[test] +fn history_activity_is_checked_after_argument_conversion() { + let mut vm = new_storage_test_vm("https://example.com/page.html"); + + let result = vm + .eval( + r#" + (() => { + const root = document.documentElement || + document.appendChild(document.createElement("html")); + const body = document.body || root.appendChild(document.createElement("body")); + const operations = [ + (history, convert) => history.go(convert(0)), + (history, convert) => history.go(convert(-1)), + (history, convert) => history.go(convert(Infinity)), + (history, convert) => { history.scrollRestoration = convert("manual"); }, + (history, convert) => history.pushState(() => {}, convert("")), + (history, convert) => history.replaceState(() => {}, "", convert("?x=2")) + ]; + return operations.map(operation => { + const frame = document.createElement("iframe"); + body.appendChild(frame); + const childHistory = frame.contentWindow.history; + let conversions = 0; + const convert = value => ({ + [Symbol.toPrimitive]() { + conversions++; + frame.remove(); + return value; + } + }); + try { + operation(childHistory, convert); + return "no throw"; + } catch (error) { + return `${error.name}:${conversions}`; + } + }).join("|"); + })() + "#, + ) + .expect("History should reject a document removed during argument conversion"); + + assert_eq!(result, ["SecurityError:1"; 6].join("|")); +} + #[test] fn history_state_preserves_structured_clone_values_not_representable_as_json() { let mut vm = new_storage_test_vm("https://example.com/base");