mirror of
https://github.com/lexmount/moli.git
synced 2026-11-06 16:07:48 +00:00
fix(events): gate raw pointer update handlers on secure contexts
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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;};
|
||||
|
||||
+90
@@ -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;
|
||||
})
|
||||
Reference in New Issue
Block a user