diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 3658dd70f9..7cccbec62e 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -478,8 +478,8 @@ pub(crate) use self::websocket::{WebSocketDispatchResult, dispatch_websocket_eve pub(crate) use self::window_events::{ WINDOW_EVENT_HANDLER_PROPERTIES, dispatch_window_error_event_with_details, dispatch_window_promise_rejection_event, dispatch_window_report_error_message, - set_window_body_onerror_handler_compiled, set_window_onerror_handler_value, - window_body_onerror_handler_is_compiled, + event_handler_property_is_exposed, set_window_body_onerror_handler_compiled, + set_window_onerror_handler_value, window_body_onerror_handler_is_compiled, }; #[cfg(test)] pub(crate) use self::window_lazy_surface::window_lazy_surface_diagnostics; diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs index c9e7ef02e2..2c9e825c0a 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs @@ -10,7 +10,9 @@ use crate::network_host::finalize_xml_http_request_event_target_realm_bindings; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RealmDependentFinalizer { + DocumentRealmBindings, NodeMixinUnscopables, + GlobalEventHandlersSecureContextSurface, CryptoSecureContextSurface, BaseAudioContextSecureContextSurface, XmlHttpRequestEventTargetState, @@ -22,7 +24,7 @@ enum RealmDependentFinalizer { const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] = &[ // The template creates @@unscopables, but its object needs a realm-local // null prototype after materialization. - ("Document", RealmDependentFinalizer::NodeMixinUnscopables), + ("Document", RealmDependentFinalizer::DocumentRealmBindings), ( "DocumentFragment", RealmDependentFinalizer::NodeMixinUnscopables, @@ -58,6 +60,18 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] = "PointerEvent", RealmDependentFinalizer::PointerEventSecureContextSurface, ), + ( + "HTMLElement", + RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface, + ), + ( + "SVGElement", + RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface, + ), + ( + "MathMLElement", + RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface, + ), ( "PerformanceObserver", RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes, @@ -87,12 +101,23 @@ pub(super) fn finalize_materialized_interface<'s>( return Ok(()); }; match finalizer { + RealmDependentFinalizer::DocumentRealmBindings => { + finalize_node_mixin_unscopables(scope, prototype)?; + crate::context_bootstrap::window_events::finalize_secure_global_event_handler_realm_bindings( + scope, prototype, + ) + } RealmDependentFinalizer::NodeMixinUnscopables => { finalize_node_mixin_unscopables(scope, prototype) } RealmDependentFinalizer::BaseAudioContextSecureContextSurface => { finalize_base_audio_context_realm_bindings(scope, prototype) } + RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface => { + crate::context_bootstrap::window_events::finalize_secure_global_event_handler_realm_bindings( + scope, prototype, + ) + } RealmDependentFinalizer::CryptoSecureContextSurface => { finalize_crypto_realm_bindings(scope, prototype) } @@ -151,6 +176,9 @@ mod tests { "XMLHttpRequestEventTarget", "Notification", "PointerEvent", + "HTMLElement", + "SVGElement", + "MathMLElement", "PerformanceObserver", ] ); diff --git a/moli-renderer-v8/src/context_bootstrap/window_events.rs b/moli-renderer-v8/src/context_bootstrap/window_events.rs index 313b25bf84..7632084d83 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events.rs @@ -8,6 +8,8 @@ mod error; mod install; mod promise; +pub(crate) const SECURE_GLOBAL_EVENT_HANDLER_PROPERTIES: &[&str] = &["onpointerrawupdate"]; + pub(crate) const SECURE_WINDOW_EVENT_HANDLER_PROPERTIES: &[&str] = &[ "ondevicemotion", "ondeviceorientation", @@ -97,7 +99,6 @@ pub(crate) const WINDOW_EVENT_HANDLER_PROPERTIES: &[&str] = &[ "onpointermove", "onpointerout", "onpointerover", - "onpointerrawupdate", "onpointerup", "onpopstate", "onprogress", @@ -160,7 +161,11 @@ pub(super) use error::window_report_error_callback; pub(crate) use error::{ dispatch_window_error_event_with_details, dispatch_window_report_error_message, }; +pub(crate) use install::event_handler_property_is_exposed; pub(super) use install::install_window_global_accessors; pub(crate) use promise::dispatch_window_promise_rejection_event; -pub(in crate::context_bootstrap) use install::install_secure_window_event_handler_accessors; +pub(in crate::context_bootstrap) use install::{ + finalize_secure_global_event_handler_realm_bindings, + install_secure_window_event_handler_accessors, +}; diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs index 6147169251..7089d308f7 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs @@ -5,9 +5,12 @@ use super::accessors::{ window_onrejectionhandled_getter_function, window_onrejectionhandled_setter_function, window_onunhandledrejection_getter_function, window_onunhandledrejection_setter_function, }; -use super::{SECURE_WINDOW_EVENT_HANDLER_PROPERTIES, WINDOW_EVENT_HANDLER_PROPERTIES}; +use super::{ + SECURE_GLOBAL_EVENT_HANDLER_PROPERTIES, SECURE_WINDOW_EVENT_HANDLER_PROPERTIES, + WINDOW_EVENT_HANDLER_PROPERTIES, +}; use crate::definitions::define_function_accessor_property; -use crate::util::v8str; +use crate::util::{get_private_value, v8str}; use moli_webapi_declare::WebApiObject; #[derive(Default, WebApiObject)] @@ -80,7 +83,10 @@ pub(crate) fn install_secure_window_event_handler_accessors<'s>( window: v8::Local<'s, v8::Object>, secure_context_available: bool, ) { - for name in SECURE_WINDOW_EVENT_HANDLER_PROPERTIES { + for name in SECURE_WINDOW_EVENT_HANDLER_PROPERTIES + .iter() + .chain(SECURE_GLOBAL_EVENT_HANDLER_PROPERTIES) + { if secure_context_available { let data = v8str(scope, name).into(); define_function_accessor_property( @@ -99,3 +105,56 @@ pub(crate) fn install_secure_window_event_handler_accessors<'s>( } } } + +pub(in crate::context_bootstrap) fn finalize_secure_global_event_handler_realm_bindings<'s>( + scope: &mut v8::PinScope<'s, '_>, + prototype: v8::Local<'s, v8::Object>, +) -> anyhow::Result<()> { + let global = scope.get_current_context().global(scope); + if window_realm_has_secure_context(scope, global) { + return Ok(()); + } + + // The isolate template is shared by secure and insecure realms. Filter + // the realm-local prototype before exposing it to author script. + for name in SECURE_GLOBAL_EVENT_HANDLER_PROPERTIES { + if !prototype + .delete(scope, v8str(scope, name).into()) + .unwrap_or(false) + { + return Err(anyhow::anyhow!( + "failed to remove secure GlobalEventHandlers member `{name}`" + )); + } + } + Ok(()) +} + +pub(crate) fn event_handler_property_is_exposed<'s>( + scope: &mut v8::PinScope<'s, '_>, + target: v8::Local<'s, v8::Object>, + property_name: &str, +) -> bool { + if !SECURE_GLOBAL_EVENT_HANDLER_PROPERTIES.contains(&property_name) + && !SECURE_WINDOW_EVENT_HANDLER_PROPERTIES.contains(&property_name) + { + return true; + } + let Some(context) = target.get_creation_context(scope) else { + return false; + }; + let global = context.global(scope); + window_realm_has_secure_context(scope, global) +} + +fn window_realm_has_secure_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value( + scope, + global, + super::super::runtime_state::WINDOW_SECURE_CONTEXT_AVAILABLE_SLOT, + ) + .is_some_and(|value| value.boolean_value(scope)) +} diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index bbf87d1ce6..cc42aa084d 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -237,6 +237,13 @@ fn invoke_event_handler_property<'s>( return; } None => { + if !crate::context_bootstrap::event_handler_property_is_exposed( + scope, + target_object, + &handler_name, + ) { + return; + } let Some(key) = v8_string(scope, &handler_name) else { return; }; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers.rs index 59eafae394..93da2667dc 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers.rs @@ -222,3 +222,20 @@ fn device_window_handler_properties_follow_secure_context_exposure() { ); } } + +#[test] +fn pointer_raw_update_handler_properties_follow_secure_context_exposure() { + for (url, secure_context) in [ + ("http://native-pointer-raw-update.test/", false), + ("https://native-pointer-raw-update.test/", true), + ("http://localhost/pointer-raw-update", true), + ("http://127.0.0.1/pointer-raw-update", true), + ] { + let mut vm = new_parsed_test_vm(url, ""); + let script = format!( + "{}({secure_context})", + include_str!("event_handlers/pointer_raw_update_secure_split.js") + ); + assert_eq!(vm.eval(&script).unwrap(), "true", "{url}"); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/native_secure_split.js b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/native_secure_split.js index 40128ea912..a6e1a27c94 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/native_secure_split.js +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/native_secure_split.js @@ -3,7 +3,15 @@ const realms=[window,document.getElementById('child').contentWindow]; for(const w of realms) for(const name of names) { const d=Object.getOwnPropertyDescriptor(w,name); - if(!w.isSecureContext){if(d)throw Error(name+': insecure exposure');continue;} + if(!w.isSecureContext){ + if(d || name in w)throw Error(name+': insecure exposure'); + let calls=0;w[name]=()=>{calls++;}; + const expando=Object.getOwnPropertyDescriptor(w,name); + if(!expando || typeof expando.value!=='function')throw Error(name+': ordinary author property'); + w.dispatchEvent(new Event(name.slice(2))); + if(calls!==0)throw Error(name+': insecure author property must stay inert'); + delete w[name];continue; + } if(!d || !d.enumerable || !d.configurable || typeof d.get!=='function' || typeof d.set!=='function' || w[name]!==null)throw Error(name+': descriptor'); if(name in w.document || name in w.document.createElement('body'))throw Error(name+': Window only'); let calls=0;w[name]=function(e){if(this!==w || e.type!==name.slice(2))throw Error(name+': receiver');calls++;return false;}; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/pointer_raw_update_secure_split.js b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/pointer_raw_update_secure_split.js new file mode 100644 index 0000000000..205e117bf6 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/pointer_raw_update_secure_split.js @@ -0,0 +1,90 @@ +(expectedSecureContext => { + const name = 'onpointerrawupdate'; + const failures = []; + const check = (condition, message) => { + if (!condition) failures.push(message); + }; + + for (const w of [window, document.getElementById('child').contentWindow]) { + check(w.isSecureContext === expectedSecureContext, 'native secure context'); + const secureDescriptor = Object.getOwnPropertyDescriptor(w, 'isSecureContext'); + let secureGetterCalls = 0; + Object.defineProperty(w, 'isSecureContext', { + configurable: true, + get() { + secureGetterCalls++; + throw new Error('author-visible isSecureContext must not control exposure'); + } + }); + + try { + const targets = [ + [w, w], + [w.document, w.Document.prototype], + [w.document.createElement('div'), w.HTMLElement.prototype], + [w.document.createElementNS('http://www.w3.org/2000/svg', 'svg'), w.SVGElement.prototype], + [w.document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math'), w.MathMLElement.prototype], + [w.document.implementation.createHTMLDocument(''), w.Document.prototype] + ]; + + for (const [target, owner] of targets) { + const label = Object.prototype.toString.call(target); + const descriptor = Object.getOwnPropertyDescriptor(owner, name); + check((name in target) === expectedSecureContext, label + ': exposure'); + + if (!expectedSecureContext) { + check(descriptor === undefined, label + ': insecure prototype exposure'); + check(target[name] === undefined, label + ': insecure value'); + let calls = 0; + target[name] = () => { calls++; }; + const expando = Object.getOwnPropertyDescriptor(target, name); + check(expando && typeof expando.value === 'function' && !expando.get, + label + ': ordinary author property'); + let listenerCalls = 0; + const listener = () => { listenerCalls++; }; + target.addEventListener('pointerrawupdate', listener); + target.dispatchEvent(new w.Event('pointerrawupdate')); + check(calls === 0, label + ': author property must not register a handler'); + check(listenerCalls === 1, label + ': explicit event listener'); + let propertyReads = 0; + Object.defineProperty(target, name, { + configurable: true, + get() { propertyReads++; return () => { calls++; }; } + }); + target.dispatchEvent(new w.Event('pointerrawupdate')); + check(propertyReads === 0 && calls === 0, label + ': author getter must stay inert'); + check(listenerCalls === 2, label + ': explicit event listener with author getter'); + target.removeEventListener('pointerrawupdate', listener); + delete target[name]; + continue; + } + + check(descriptor && descriptor.enumerable && descriptor.configurable && + typeof descriptor.get === 'function' && typeof descriptor.set === 'function', + label + ': secure accessor'); + check(target[name] === null, label + ': initial value'); + let calls = 0; + const handler = function(event) { + check(this === target && event.type === 'pointerrawupdate', label + ': callback receiver'); + calls++; + return false; + }; + target[name] = handler; + check(target[name] === handler, label + ': callback identity'); + check(!target.dispatchEvent(new w.Event('pointerrawupdate', {cancelable: true})) && calls === 1, + label + ': registered handler'); + target[name] = null; + check(target[name] === null, label + ': cleared value'); + check(target.dispatchEvent(new w.Event('pointerrawupdate', {cancelable: true})) && calls === 1, + label + ': cleared handler'); + } + + check(secureGetterCalls === 0, 'exposure must use trusted realm state'); + } finally { + Object.defineProperty(w, 'isSecureContext', secureDescriptor); + } + } + + if (failures.length) throw new Error(failures.join('\n')); + return true; +})