From b3ef2ec3e33a9cb413795c232c99f3811ff70d3d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 1 Oct 2026 00:18:28 +0800 Subject: [PATCH] fix(webidl): validate Event receivers with native brands An invalid receiver can reach Event callbacks, including a native initEvent path that can abort the process. Enable the existing WebIDL receiver checks on Event and the event interfaces already present on main, before callbacks and argument conversion. Genuine events and inherited event interfaces remain usable across realms. The regression matrix rejects forged objects, author and revoked Proxies without evaluating conversion hooks or Proxy traps, and checks TypeError in the callee realm. Existing spoofing tests retain their real-object assertions and now require TypeError for invalid receivers. --- .../src/context_bootstrap/event_template.rs | 28 ++-- .../event_dispatch_and_targets.rs | 28 ++-- .../src/script_vm/tests/browser_api/media.rs | 7 +- .../src/script_vm/tests/event_receivers.js | 125 ++++++++++++++++++ .../src/script_vm/tests/event_receivers.rs | 15 +++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + 6 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/event_receivers.js create mode 100644 moli-renderer-v8/src/script_vm/tests/event_receivers.rs diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 4e147f6f33..5557a7c127 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -37,7 +37,7 @@ pub(in crate::context_bootstrap) fn object_is_event_target<'s>( } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::Event, enumerable)] +#[webapi(interface = web_api_interfaces::Event, enumerable, receiver)] struct EventBaseTemplateMethodsDeclaration { #[webapi(accessor_property = "type", getter = event_type_getter_function)] event_type: (), @@ -131,7 +131,7 @@ struct EventBaseTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CloseEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CloseEvent, enumerable, receiver)] struct CloseEventTemplateAccessorsDeclaration { #[webapi(accessor_property = "wasClean", getter = close_event_was_clean_getter_function)] was_clean: (), @@ -144,28 +144,28 @@ struct CloseEventTemplateAccessorsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::TrackEvent, enumerable)] +#[webapi(interface = web_api_interfaces::TrackEvent, enumerable, receiver)] struct TrackEventTemplateAccessorsDeclaration { #[webapi(accessor_property, getter = track_event_track_getter_function)] track: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::SubmitEvent, enumerable)] +#[webapi(interface = web_api_interfaces::SubmitEvent, enumerable, receiver)] struct SubmitEventTemplateAccessorsDeclaration { #[webapi(accessor_property, getter = submit_event_submitter_getter_function)] submitter: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::FormDataEvent, enumerable)] +#[webapi(interface = web_api_interfaces::FormDataEvent, enumerable, receiver)] struct FormDataEventTemplateAccessorsDeclaration { #[webapi(accessor_property = "formData", getter = form_data_event_form_data_getter_function)] form_data: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::UIEvent, enumerable)] +#[webapi(interface = web_api_interfaces::UIEvent, enumerable, receiver)] struct UiEventTemplateMethodsDeclaration { #[webapi(method = "initUIEvent", length = 0, callback = ui_event_init_callback)] init_ui_event: (), @@ -178,7 +178,7 @@ struct UiEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::FocusEvent, enumerable)] +#[webapi(interface = web_api_interfaces::FocusEvent, enumerable, receiver)] struct FocusEventTemplateAccessorsDeclaration { #[webapi( accessor_property = "relatedTarget", @@ -188,14 +188,14 @@ struct FocusEventTemplateAccessorsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::TextEvent, enumerable)] +#[webapi(interface = web_api_interfaces::TextEvent, enumerable, receiver)] struct TextEventTemplateMethodsDeclaration { #[webapi(method = "initTextEvent", length = 1, callback = text_event_init_callback)] init_text_event: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CompositionEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CompositionEvent, enumerable, receiver)] struct CompositionEventTemplateMethodsDeclaration { #[webapi( method = "initCompositionEvent", @@ -206,14 +206,14 @@ struct CompositionEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CustomEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CustomEvent, enumerable, receiver)] struct CustomEventTemplateMethodsDeclaration { #[webapi(method = "initCustomEvent", length = 1, callback = custom_event_init_callback)] init_custom_event: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::StorageEvent, enumerable)] +#[webapi(interface = web_api_interfaces::StorageEvent, enumerable, receiver)] struct StorageEventTemplateMethodsDeclaration { #[webapi( method = "initStorageEvent", @@ -224,7 +224,7 @@ struct StorageEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::KeyboardEvent, enumerable)] +#[webapi(interface = web_api_interfaces::KeyboardEvent, enumerable, receiver)] struct KeyboardEventTemplateMethodsDeclaration { #[webapi(method = "initKeyboardEvent", length = 7, callback = keyboard_event_init_callback)] init_keyboard_event: (), @@ -238,7 +238,7 @@ struct KeyboardEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::MouseEvent, enumerable)] +#[webapi(interface = web_api_interfaces::MouseEvent, enumerable, receiver)] struct MouseEventTemplateMethodsDeclaration { #[webapi( accessor_property = "relatedTarget", @@ -270,7 +270,7 @@ struct WheelEventTemplateConstantsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::PointerEvent, enumerable)] +#[webapi(interface = web_api_interfaces::PointerEvent, enumerable, receiver)] struct PointerEventTemplateMethodsDeclaration { #[webapi( method = "getPredictedEvents", diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs index a321ec653d..6b6936c9e8 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs @@ -1084,7 +1084,12 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { }, { once: true }); simpleTarget.dispatchEvent(simple); - const fakePath = Event.prototype.composedPath.call({ __lmCp: ["fake"] }); + let fakePathThrowsTypeError = false; + try { + Event.prototype.composedPath.call({ __lmCp: ["fake"] }); + } catch (error) { + fakePathThrowsTypeError = error instanceof TypeError; + } return JSON.stringify({ cleanBeforeOwn, cleanDuring, @@ -1094,7 +1099,7 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { spoofedAfterLength: spoofed.composedPath().length, simpleDuring, simpleAfterLength: simple.composedPath().length, - fakePathLength: fakePath.length + fakePathThrowsTypeError }); })() "#, @@ -1103,7 +1108,7 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { assert_eq!( result, - r#"{"cleanBeforeOwn":false,"cleanDuring":{"ownNameVisible":false,"firstIsTarget":true,"hasPath":true},"cleanAfterLength":0,"spoofedBeforeLength":0,"spoofedDuring":{"ownValue":"spoofed-during","firstIsTarget":true,"containsSpoof":false},"spoofedAfterLength":0,"simpleDuring":{"ownValue":"spoofed-simple","firstIsSimpleTarget":true,"length":1,"containsSpoof":false},"simpleAfterLength":0,"fakePathLength":0}"# + r#"{"cleanBeforeOwn":false,"cleanDuring":{"ownNameVisible":false,"firstIsTarget":true,"hasPath":true},"cleanAfterLength":0,"spoofedBeforeLength":0,"spoofedDuring":{"ownValue":"spoofed-during","firstIsTarget":true,"containsSpoof":false},"spoofedAfterLength":0,"simpleDuring":{"ownValue":"spoofed-simple","firstIsSimpleTarget":true,"length":1,"containsSpoof":false},"simpleAfterLength":0,"fakePathThrowsTypeError":true}"# ); } @@ -1290,6 +1295,11 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { .eval( r#" (() => { + const rejectsReceiver = (getter, receiver) => { + try { getter.call(receiver); } + catch (error) { return error instanceof TypeError; } + return false; + }; const getterDescriptor = (prototype, name) => { const descriptor = Object.getOwnPropertyDescriptor(prototype, name); return [ @@ -1377,20 +1387,20 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { close.__moliCloseEventReason ], fakeClose: [ - closeWasCleanGetter.call(fakeClose), - closeCodeGetter.call(fakeClose), - closeReasonGetter.call(fakeClose) + rejectsReceiver(closeWasCleanGetter, fakeClose), + rejectsReceiver(closeCodeGetter, fakeClose), + rejectsReceiver(closeReasonGetter, fakeClose) ], submitOwnBefore, submitterDescriptor: getterDescriptor(SubmitEvent.prototype, "submitter"), submitterIsButton: submit.submitter === button, submitSpoofIsInput: submit.__moliSubmitEventSubmitter instanceof HTMLInputElement, - fakeSubmitterIsNull: submitterGetter.call(fakeSubmit) === null, + fakeSubmitterThrowsTypeError: rejectsReceiver(submitterGetter, fakeSubmit), formDataOwnBefore, formDataDescriptor: getterDescriptor(FormDataEvent.prototype, "formData"), formDataIsReal: formDataEvent.formData === formData, formDataSpoofIsSpoofed: formDataEvent.__moliFormDataEventFormData === spoofedFormData, - fakeFormDataIsUndefined: formDataGetter.call(fakeFormDataEvent) === undefined + fakeFormDataThrowsTypeError: rejectsReceiver(formDataGetter, fakeFormDataEvent) }); })() "#, @@ -1399,7 +1409,7 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { assert_eq!( result, - r#"{"closeDescriptors":["wasClean:function:get wasClean:0:undefined:true:true","code:function:get code:0:undefined:true:true","reason:function:get reason:0:undefined:true:true"],"closeOwnBefore":[],"closeValues":[true,1000,"done"],"closeSpoofValues":[false,4000,"spoofed"],"fakeClose":[false,0,""],"submitOwnBefore":[],"submitterDescriptor":"submitter:function:get submitter:0:undefined:true:true","submitterIsButton":true,"submitSpoofIsInput":true,"fakeSubmitterIsNull":true,"formDataOwnBefore":[],"formDataDescriptor":"formData:function:get formData:0:undefined:true:true","formDataIsReal":true,"formDataSpoofIsSpoofed":true,"fakeFormDataIsUndefined":true}"# + r#"{"closeDescriptors":["wasClean:function:get wasClean:0:undefined:true:true","code:function:get code:0:undefined:true:true","reason:function:get reason:0:undefined:true:true"],"closeOwnBefore":[],"closeValues":[true,1000,"done"],"closeSpoofValues":[false,4000,"spoofed"],"fakeClose":[true,true,true],"submitOwnBefore":[],"submitterDescriptor":"submitter:function:get submitter:0:undefined:true:true","submitterIsButton":true,"submitSpoofIsInput":true,"fakeSubmitterThrowsTypeError":true,"formDataOwnBefore":[],"formDataDescriptor":"formData:function:get formData:0:undefined:true:true","formDataIsReal":true,"formDataSpoofIsSpoofed":true,"fakeFormDataThrowsTypeError":true}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs index 11ab83f9c8..f47e2e8190 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs @@ -1174,6 +1174,9 @@ fn track_event_track_uses_prototype_private_slot() { const ownBefore = Object.getOwnPropertyNames(event).filter(name => name === 'track'); event.track = second; const fake = { track: second }; + let fakeTrackThrowsTypeError = false; + try { descriptor.get.call(fake); } + catch (error) { fakeTrackThrowsTypeError = error instanceof TypeError; } return JSON.stringify({ hasGetter: typeof descriptor.get === 'function', @@ -1186,7 +1189,7 @@ fn track_event_track_uses_prototype_private_slot() { trackAfterAssignment: event.track === first, assignmentCreatedOwn: Object.prototype.hasOwnProperty.call(event, 'track'), emptyTrackIsNull: empty.track === null, - fakeTrackIsNull: descriptor.get.call(fake) === null + fakeTrackThrowsTypeError }); })() "#, @@ -1195,7 +1198,7 @@ fn track_event_track_uses_prototype_private_slot() { assert_eq!( result, - r#"{"hasGetter":true,"getterName":"get track","getterLength":0,"hasSetter":true,"enumerable":true,"configurable":true,"ownBefore":[],"trackAfterAssignment":true,"assignmentCreatedOwn":false,"emptyTrackIsNull":true,"fakeTrackIsNull":true}"# + r#"{"hasGetter":true,"getterName":"get track","getterLength":0,"hasSetter":true,"enumerable":true,"configurable":true,"ownBefore":[],"trackAfterAssignment":true,"assignmentCreatedOwn":false,"emptyTrackIsNull":true,"fakeTrackThrowsTypeError":true}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/event_receivers.js b/moli-renderer-v8/src/script_vm/tests/event_receivers.js new file mode 100644 index 0000000000..7ccba7350f --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/event_receivers.js @@ -0,0 +1,125 @@ +(() => { + const other = document.getElementById('child').contentWindow; + const realms = [['main', globalThis], ['iframe', other]]; + const rows = []; + function check(name, expected, run) { + let actual; + try { actual = run(); } catch (error) { actual = {error: error.name, message: error.message}; } + rows.push({name, expected, actual, pass: JSON.stringify(actual) === JSON.stringify(expected)}); + } + const coreGetters = ['type', 'target', 'currentTarget', 'eventPhase', 'bubbles', 'cancelable', + 'defaultPrevented', 'composed', 'srcElement', 'cancelBubble', 'returnValue', 'timeStamp']; + const methods = ['preventDefault', 'stopPropagation', 'stopImmediatePropagation', 'composedPath']; + function invalidReceivers(realm, real, traps) { + const revoked = Proxy.revocable(real, {}); revoked.revoke(); + return [null, undefined, 0, {}, realm, realm.document, realm.document.createElement('div'), + Object.getPrototypeOf(real), Object.create(real), new Proxy(real, {}), revoked.proxy, + new Proxy(real, {get() {traps.push('get'); throw new Error('get trap');}, + getPrototypeOf() {traps.push('prototype'); throw new Error('prototype trap');}})]; + } + for (const [calleeName, callee] of realms) for (const [homeName, home] of realms) { + const label = calleeName + '/' + homeName; + for (const kind of ['Event', 'MouseEvent', 'CustomEvent']) { + const real = new home[kind]('before', {cancelable: true}); + for (const name of coreGetters) { + const getter = Object.getOwnPropertyDescriptor(callee.Event.prototype, name).get; + check(label + '/' + kind + '/' + name + '/genuine', true, () => getter.call(real) === real[name]); + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/invalid-' + index, true, () => { + try { getter.call(receiver); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/no-traps', [], () => traps); + } + for (const name of methods) { + const method = callee.Event.prototype[name]; + check(label + '/' + kind + '/' + name + '/genuine', true, () => { + method.call(new home[kind]('before', {cancelable: true})); return true; + }); + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/invalid-' + index, true, () => { + try { method.call(receiver); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/no-traps', [], () => traps); + } + for (const name of ['cancelBubble', 'returnValue']) { + const setter = Object.getOwnPropertyDescriptor(callee.Event.prototype, name).set; + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/setter-invalid-' + index, true, () => { + try { setter.call(receiver, false); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/setter-no-traps', [], () => traps); + } + } + const legacy = [['Event', 'initEvent'], ['UIEvent', 'initUIEvent'], ['MouseEvent', 'initMouseEvent'], + ['KeyboardEvent', 'initKeyboardEvent'], ['CustomEvent', 'initCustomEvent'], + ['StorageEvent', 'initStorageEvent'], ['CompositionEvent', 'initCompositionEvent'], ['TextEvent', 'initTextEvent']]; + for (const [kind, name] of legacy) { + const make = () => { + if (kind !== 'TextEvent') return new home[kind]('before'); + const event = home.document.createEvent(kind); + event.initTextEvent('before'); + return event; + }; + const method = callee[kind].prototype[name]; + const args = type => name === 'initMouseEvent' + ? [type, true, true, null, 0, 0, 0, 0, 0, false, false, false, false, 0, null] + : [type, true, true]; + check(label + '/' + name + '/genuine', ['after', true], () => { + const real = make(); method.apply(real, args('after')); + return [real.type, real instanceof home[kind]]; + }); + const real = make(), traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + name + '/invalid-' + index, [true, 0], () => { + let conversions = 0, rejected = false; + const type = {toString() {conversions++; return 'forged';}}; + try {method.apply(receiver, args(type));} catch(error) {rejected = error instanceof callee.TypeError;} + return [rejected, conversions]; + }); + } + check(label + '/' + name + '/no-traps', [], () => traps); + if (kind !== 'Event') { + check(label + '/' + name + '/wrong-event-brand', true, () => { + try {method.apply(new home.Event('before'), args('after'));} + catch(error) {return error instanceof callee.TypeError;} + return false; + }); + } + check(label + '/' + name + '/conversion-exception', true, () => { + const marker = new Error('conversion'); + try {method.apply(make(), args({toString() {throw marker;}}));} + catch(error) {return error === marker;} + return false; + }); + check(label + '/' + name + '/dispatch-no-mutation', ['before', false], () => { + const real = make(), target = new home.EventTarget(); + target.addEventListener(real.type, () => method.apply(real, args('after'))); + target.dispatchEvent(real); + return [real.type, real.bubbles]; + }); + } + check(label + '/borrowed-init-on-subclass', ['changed', true, true], () => { + const real = new home.MouseEvent('before'); + callee.Event.prototype.initEvent.call(real, 'changed', true, true); + return [real.type, real instanceof home.MouseEvent, real.cancelable]; + }); + check(label + '/inherited-ui-init-on-mouse', ['changed', true], () => { + const real = new home.MouseEvent('before'); + callee.UIEvent.prototype.initUIEvent.call(real, 'changed'); + return [real.type, real instanceof home.MouseEvent]; + }); + } + globalThis.__eventReceiverFailures = rows.filter(row => !row.pass); + globalThis.__nodeReplacementResults = {total: rows.length, passed: rows.filter(row => row.pass).length, + failures: __eventReceiverFailures, rows}; + return rows.every(row => row.pass); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/event_receivers.rs b/moli-renderer-v8/src/script_vm/tests/event_receivers.rs new file mode 100644 index 0000000000..9884827495 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/event_receivers.rs @@ -0,0 +1,15 @@ +use super::*; + +#[test] +fn event_bindings_validate_native_receivers_before_conversion_in_the_callee_realm() { + let mut vm = new_parsed_test_vm( + "https://event-receivers.test/", + "", + ); + vm.eval(include_str!("event_receivers.js")).unwrap(); + assert_eq!( + vm.eval("JSON.stringify(globalThis.__eventReceiverFailures)") + .unwrap(), + "[]" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 7cf085e16c..bf96eff0ba 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2133,6 +2133,7 @@ mod device_events; mod dom_elements; mod dom_xhr; mod element_click; +mod event_receivers; mod headers_list; mod http_fixture; mod import_meta;