mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 00:08:29 +00:00
fix(forms): validate FormData native argument brands
This commit is contained in:
@@ -10,10 +10,11 @@ use crate::{
|
||||
native_bridge::{
|
||||
JsContextHost,
|
||||
element::{form_associated_form_owner, is_valid_submit_button},
|
||||
node_runtime_and_handle_from_object, throw_dom_exception,
|
||||
node_relevant_context, node_runtime_and_handle_from_object_or_detached,
|
||||
throw_dom_exception,
|
||||
},
|
||||
util::serialize_v8_iter_array,
|
||||
webidl,
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
@@ -115,77 +116,82 @@ pub(super) fn form_data_constructor_callback<'s>(
|
||||
return;
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let Some(parsed) = webidl::parse_args::<FormDataConstructorArgs<'s>>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if let Some(form) = parsed.form {
|
||||
let Ok(form) = v8::Local::<v8::Object>::try_from(form) else {
|
||||
throw_type_error(scope, "FormData constructor requires an HTMLFormElement");
|
||||
return;
|
||||
};
|
||||
if object_string_property_defined(scope, form, "tagName")
|
||||
.is_none_or(|tag| !tag.eq_ignore_ascii_case("form"))
|
||||
{
|
||||
throw_type_error(scope, "FormData constructor requires an HTMLFormElement");
|
||||
return;
|
||||
let form = match parsed.form {
|
||||
Some(value) => {
|
||||
let Ok(form) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
throw_type_error(scope, "FormData constructor requires an HTMLFormElement");
|
||||
return;
|
||||
};
|
||||
if !web_api_interfaces::HTMLFormElement::is_instance(scope, form) {
|
||||
throw_type_error(scope, "FormData constructor requires an HTMLFormElement");
|
||||
return;
|
||||
}
|
||||
Some(form)
|
||||
}
|
||||
let Ok((form_runtime_ptr, form_handle)) = node_runtime_and_handle_from_object(scope, form)
|
||||
None => None,
|
||||
};
|
||||
// WebIDL converts the optional submitter even when the form is omitted.
|
||||
let submitter = match parsed.submitter {
|
||||
Some(value) if !value.is_null_or_undefined() => {
|
||||
let Ok(submitter) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be an HTMLElement or null",
|
||||
);
|
||||
return;
|
||||
};
|
||||
if !web_api_interfaces::HTMLElement::is_instance(scope, submitter) {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be an HTMLElement or null",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Some(submitter)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let mut entries = Vec::new();
|
||||
if let Some(form) = form {
|
||||
let Ok((form_runtime_ptr, form_handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, form)
|
||||
else {
|
||||
throw_type_error(scope, "FormData constructor requires an HTMLFormElement");
|
||||
return;
|
||||
};
|
||||
let submitter = match parsed.submitter {
|
||||
Some(value) if value.is_null_or_undefined() => None,
|
||||
Some(value) => match v8::Local::<v8::Object>::try_from(value) {
|
||||
Ok(submitter) => {
|
||||
let Ok((submitter_runtime_ptr, submitter_handle)) =
|
||||
node_runtime_and_handle_from_object(scope, submitter)
|
||||
else {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be a submit button",
|
||||
);
|
||||
return;
|
||||
};
|
||||
if submitter_runtime_ptr != form_runtime_ptr {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotFoundError",
|
||||
8,
|
||||
"The specified element is not owned by this form element.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let runtime = unsafe { &*form_runtime_ptr };
|
||||
if !is_valid_submit_button(runtime, submitter_handle) {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be a submit button",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if form_associated_form_owner(runtime, submitter_handle) != Some(form_handle) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotFoundError",
|
||||
8,
|
||||
"The specified element is not owned by this form element.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Some(submitter)
|
||||
}
|
||||
Err(_) => {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be an HTMLElement or null",
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if let Some(submitter) = submitter {
|
||||
let Ok((submitter_runtime_ptr, submitter_handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, submitter)
|
||||
else {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be a submit button",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let runtime = unsafe { &*submitter_runtime_ptr };
|
||||
if !is_valid_submit_button(runtime, submitter_handle) {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"FormData constructor submitter must be a submit button",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if submitter_runtime_ptr != form_runtime_ptr
|
||||
|| form_associated_form_owner(runtime, submitter_handle) != Some(form_handle)
|
||||
{
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotFoundError",
|
||||
8,
|
||||
"The specified element is not owned by this form element.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(next_entries) = construct_form_data_entries_for_form(
|
||||
scope,
|
||||
form_runtime_ptr,
|
||||
@@ -219,6 +225,8 @@ pub(crate) fn construct_form_data_entries_for_form<'s>(
|
||||
return None;
|
||||
}
|
||||
|
||||
let context = node_relevant_context(scope, form).unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let entries = serialize_form_data_controls(scope, runtime_ptr, form_handle, submitter);
|
||||
let entries = dispatch_form_data_event_with_entries(scope, form, &entries).unwrap_or(entries);
|
||||
unsafe { &mut *runtime_ptr }.end_form_data_construction(form_handle);
|
||||
@@ -252,7 +260,7 @@ fn dispatch_form_data_event<'s>(
|
||||
form_data: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let Ok((runtime_ptr, form_handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object(scope, form)
|
||||
node_runtime_and_handle_from_object_or_detached(scope, form)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -7,10 +7,10 @@ use crate::dom::{
|
||||
};
|
||||
use crate::native_bridge::{
|
||||
element::{
|
||||
element_attribute_for_object, element_internals_form_value_for_target,
|
||||
control_has_datalist_ancestor, element_internals_form_value_for_target,
|
||||
form_control_is_effectively_disabled, form_data_control_elements, text_control_value,
|
||||
},
|
||||
node_runtime_and_handle_from_object,
|
||||
node_runtime_and_handle_from_object_or_detached,
|
||||
};
|
||||
use moli_encoding::is_charset_sentinel_name;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
@@ -31,6 +31,9 @@ pub(super) fn serialize_form_data_controls<'s>(
|
||||
let mut entries = Vec::new();
|
||||
let controls = form_data_control_elements(unsafe { &*runtime_ptr }, form_handle);
|
||||
for handle in controls {
|
||||
if control_has_datalist_ancestor(unsafe { &*runtime_ptr }, handle) {
|
||||
continue;
|
||||
}
|
||||
let Some(control) = form_data_control_object(scope, runtime_ptr, handle) else {
|
||||
continue;
|
||||
};
|
||||
@@ -82,10 +85,6 @@ fn append_form_data_entries_for_control<'s>(
|
||||
if control_is_effectively_disabled(scope, control) {
|
||||
return;
|
||||
}
|
||||
if control_has_datalist_ancestor(scope, control) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tag = object_string_property_defined(scope, control, "tagName")
|
||||
.map(|tag| tag.to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
@@ -243,19 +242,28 @@ fn append_form_data_entries_for_control<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn form_control_name(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
control: v8::Local<'_, v8::Object>,
|
||||
fn form_control_name<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> String {
|
||||
element_attribute_for_object(scope, control, "name").unwrap_or_default()
|
||||
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, control)
|
||||
else {
|
||||
return String::new();
|
||||
};
|
||||
unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.and_then(Node::as_element)
|
||||
.and_then(|element| element.attribute_ns("", "name"))
|
||||
.unwrap_or_default()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn image_submitter_coordinates(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
control: v8::Local<'_, v8::Object>,
|
||||
fn image_submitter_coordinates<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> (u32, u32) {
|
||||
let Ok((runtime_ptr, handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object(scope, control)
|
||||
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, control)
|
||||
else {
|
||||
return (0, 0);
|
||||
};
|
||||
@@ -381,9 +389,9 @@ fn push_string_form_data_entry(
|
||||
push_form_data_entry(entries, name, v8::Global::new(scope, value));
|
||||
}
|
||||
|
||||
fn form_control_value(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
control: v8::Local<'_, v8::Object>,
|
||||
fn form_control_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
default: &str,
|
||||
) -> String {
|
||||
if let Some(value) = native_form_control_value(scope, control) {
|
||||
@@ -392,11 +400,12 @@ fn form_control_value(
|
||||
object_string_property_defined(scope, control, "value").unwrap_or_else(|| default.to_owned())
|
||||
}
|
||||
|
||||
fn native_form_control_value(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
control: v8::Local<'_, v8::Object>,
|
||||
fn native_form_control_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
let (runtime_ptr, handle) = node_runtime_and_handle_from_object(scope, control).ok()?;
|
||||
let (runtime_ptr, handle) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, control).ok()?;
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let element = runtime.dom_host().node(handle).and_then(Node::as_element)?;
|
||||
if element.is_html_input() {
|
||||
@@ -427,7 +436,9 @@ fn control_is_effectively_disabled<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
if let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, control) {
|
||||
if let Ok((runtime_ptr, handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, control)
|
||||
{
|
||||
return form_control_is_effectively_disabled(unsafe { &*runtime_ptr }, handle);
|
||||
}
|
||||
if object_bool_property(scope, control, "disabled").unwrap_or(false) {
|
||||
@@ -453,7 +464,8 @@ fn native_form_associated_custom_element<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, control) else {
|
||||
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, control)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
is_form_associated_custom_element_handle(unsafe { &*runtime_ptr }, handle)
|
||||
@@ -488,23 +500,6 @@ fn control_is_in_first_legend<'s>(
|
||||
false
|
||||
}
|
||||
|
||||
fn control_has_datalist_ancestor<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
control: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
let mut current = object_property_as_object(scope, control, "parentElement");
|
||||
while let Some(element) = current {
|
||||
let tag = object_string_property_defined(scope, element, "tagName")
|
||||
.map(|tag| tag.to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
if tag == "datalist" {
|
||||
return true;
|
||||
}
|
||||
current = object_property_as_object(scope, element, "parentElement");
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn option_is_disabled(scope: &mut v8::PinScope<'_, '_>, option: v8::Local<'_, v8::Object>) -> bool {
|
||||
let mut current = Some(option);
|
||||
while let Some(element) = current {
|
||||
|
||||
@@ -105,9 +105,9 @@ use trusted_types::{
|
||||
};
|
||||
|
||||
pub(crate) use forms::{
|
||||
autocomplete_field_name, autofill_related_form_control_elements, form_associated_form_owner,
|
||||
form_control_elements, form_data_control_elements, is_valid_submit_button,
|
||||
submit_form_with_submit_event,
|
||||
autocomplete_field_name, autofill_related_form_control_elements, control_has_datalist_ancestor,
|
||||
form_associated_form_owner, form_control_elements, form_data_control_elements,
|
||||
is_valid_submit_button, submit_form_with_submit_event,
|
||||
};
|
||||
pub(in crate::native_bridge) use forms::{form_named_control_matches, form_named_image_matches};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -245,6 +245,7 @@ pub(in crate::native_bridge) use self::text_control::{
|
||||
text_control_set_selection_range_callback, textarea_value_getter_function,
|
||||
textarea_value_setter_function,
|
||||
};
|
||||
pub(crate) use self::validation::control_has_datalist_ancestor;
|
||||
pub(crate) use self::validation::v8_pattern_is_usable;
|
||||
pub(in crate::native_bridge) use self::validation::{
|
||||
control_check_validity_callback, control_matches_validity_pseudo,
|
||||
|
||||
@@ -488,7 +488,7 @@ fn required_value_missing_is_suppressed_for_immutable_control(
|
||||
|| (element.is_html_input() && element.input_type().supports_readonly()))
|
||||
}
|
||||
|
||||
fn control_has_datalist_ancestor(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
pub(crate) fn control_has_datalist_ancestor(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
let mut current = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
let Some(parent_element) = runtime.dom_host().node(parent).and_then(Node::as_element)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
(() => {
|
||||
const globals = [window, document.getElementById('child').contentWindow];
|
||||
const rows = [], errors = [];
|
||||
for (let realm = 0; realm < globals.length; ++realm) {
|
||||
const g = globals[realm], other = globals[1 - realm];
|
||||
for (const kind of ['live', 'windowless', 'parsed']) {
|
||||
const label = `${realm}/${kind}`;
|
||||
try {
|
||||
const d = kind === 'live' ? g.document : kind === 'windowless' ?
|
||||
g.document.implementation.createHTMLDocument('') :
|
||||
new g.DOMParser().parseFromString('<!doctype html><body>', 'text/html');
|
||||
const form = d.createElement('form'), input = d.createElement('input');
|
||||
input.setAttribute('name', 'input-name'); input.setAttribute('value', 'input-value');
|
||||
form.appendChild(input);
|
||||
const select = d.createElement('select'), option = d.createElement('option');
|
||||
select.setAttribute('name', 'select-name'); option.setAttribute('value', 'option-value');
|
||||
select.appendChild(option); form.appendChild(select);
|
||||
const button = d.createElement('button');
|
||||
button.setAttribute('type', 'submit'); button.setAttribute('name', 'send'); button.setAttribute('value', 'button');
|
||||
form.appendChild(button);
|
||||
let tagReads = 0;
|
||||
Object.defineProperty(form, 'tagName', {configurable: true, get() { ++tagReads; throw new Error('author tag getter'); }});
|
||||
let valueReads = 0;
|
||||
Object.defineProperty(input, 'value', {configurable: true, get() { ++valueReads; throw new Error('author value getter'); }});
|
||||
let eventCount = 0, eventRealm = true, eventTarget = true;
|
||||
form.addEventListener('formdata', event => {
|
||||
++eventCount;
|
||||
eventRealm &&= event instanceof g.FormDataEvent && event.formData instanceof g.FormData;
|
||||
eventTarget &&= event.target === form;
|
||||
event.formData.append('from-handler', 'listener');
|
||||
});
|
||||
const data = new other.FormData(form);
|
||||
const checks = {constructorRealm: Object.getPrototypeOf(data) === other.FormData.prototype,
|
||||
input: data.get('input-name') === 'input-value',
|
||||
option: data.get('select-name') === 'option-value',
|
||||
event: eventCount === 1 && eventRealm && eventTarget,
|
||||
handlerMutation: data.get('from-handler') === 'listener',
|
||||
nativeProperties: tagReads === 0 && valueReads === 0,
|
||||
omitSubmitter: !data.has('send')};
|
||||
const withSubmitter = new other.FormData(form, button);
|
||||
checks.submitter = withSubmitter.get('send') === 'button' && eventCount === 2;
|
||||
let traps = 0;
|
||||
const author = new g.Proxy(form, {get() { ++traps; return undefined; }, getPrototypeOf() { ++traps; return null; }});
|
||||
const revoked = g.Proxy.revocable(form, {}); revoked.revoke();
|
||||
checks.invalidForms = true;
|
||||
for (const invalid of [null, {}, g.Object.create(form), author, revoked.proxy,
|
||||
d.createElement('div'), d.createElementNS('http://www.w3.org/2000/svg', 'form')]) {
|
||||
let error;
|
||||
try { new other.FormData(invalid); } catch (caught) { error = caught; }
|
||||
checks.invalidForms &&= error instanceof other.TypeError;
|
||||
}
|
||||
checks.invalidSubmitters = true;
|
||||
for (const invalid of [{}, g.Object.create(button), new g.Proxy(button, {get() { ++traps; }}),
|
||||
d.createElement('div'), other.document.createElement('div')]) {
|
||||
let error;
|
||||
try { new other.FormData(form, invalid); } catch (caught) { error = caught; }
|
||||
checks.invalidSubmitters &&= error instanceof other.TypeError;
|
||||
}
|
||||
const foreign = d.createElement('button'); foreign.type = 'submit';
|
||||
const crossDocument = other.document.createElement('button'); crossDocument.type = 'submit';
|
||||
checks.foreignSubmitter = true;
|
||||
for (const submitter of [foreign, crossDocument]) {
|
||||
let notFound;
|
||||
try { new other.FormData(form, submitter); } catch (caught) { notFound = caught; }
|
||||
checks.foreignSubmitter &&= notFound instanceof other.DOMException && notFound.name === 'NotFoundError';
|
||||
}
|
||||
let omittedSubmitterError;
|
||||
try { new other.FormData(undefined, {}); }
|
||||
catch (error) { omittedSubmitterError = error; }
|
||||
checks.omittedFormStillConvertsSubmitter = omittedSubmitterError instanceof other.TypeError;
|
||||
checks.noPublicReads = traps === 0 && tagReads === 0 && valueReads === 0;
|
||||
other.document.adoptNode(form);
|
||||
const adoptedData = new other.FormData(form);
|
||||
checks.adoptedFormRealm = eventCount === 3 && eventRealm && eventTarget &&
|
||||
Object.getPrototypeOf(adoptedData) === other.FormData.prototype &&
|
||||
adoptedData.get('from-handler') === 'listener';
|
||||
rows.push({label, checks});
|
||||
} catch (error) { errors.push({label, message: String(error)}); }
|
||||
}
|
||||
}
|
||||
globalThis.__uiEventResults = {rows, errors,
|
||||
passed: errors.length === 0 && rows.every(row => Object.values(row.checks).every(value => value === true))};
|
||||
return __uiEventResults.passed;
|
||||
})()
|
||||
@@ -0,0 +1,33 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn form_data_constructor_accepts_native_forms_across_document_realms() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://form-data-brands.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.eval(
|
||||
r#"
|
||||
if (!document.documentElement) document.appendChild(document.createElement('html'));
|
||||
if (!document.body) document.documentElement.appendChild(document.createElement('body'));
|
||||
const frame=document.body.appendChild(document.createElement('iframe'));
|
||||
frame.id='child'; frame.srcdoc='<body></body>'; void frame.contentWindow;
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
vm.run_one_child_frame_task_executor_turn(
|
||||
ChildFrameSemanticTurnKind::RealmMaterialization,
|
||||
&loader,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(include_str!("form_data_brands.js")).unwrap(), "true", "{}",
|
||||
vm.eval("JSON.stringify({errors:__uiEventResults.errors,failures:__uiEventResults.rows.filter(row=>Object.values(row.checks).some(value=>value!==true))})").unwrap());
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod custom_elements;
|
||||
mod detached;
|
||||
mod dom_surface;
|
||||
mod focus;
|
||||
mod form_data_brands;
|
||||
mod live_document;
|
||||
mod text_controls;
|
||||
mod wrapper_identity;
|
||||
|
||||
Reference in New Issue
Block a user