fix(events): gate raw pointer update handlers on secure contexts

This commit is contained in:
ldm0
2026-10-09 19:06:50 +08:00
committed by Donough Liu
parent 3f17ed760f
commit e768ed37de
8 changed files with 223 additions and 9 deletions
+2 -2
View File
@@ -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;
@@ -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",
]
);
@@ -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,
};
@@ -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))
}
+7
View File
@@ -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;
};
@@ -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, "<body><iframe id=child></iframe></body>");
let script = format!(
"{}({secure_context})",
include_str!("event_handlers/pointer_raw_update_secure_split.js")
);
assert_eq!(vm.eval(&script).unwrap(), "true", "{url}");
}
}
@@ -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;};
@@ -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;
})