fix(js): validate the submitter argument to requestSubmit() (#424) (#426)

HTMLFormElement.requestSubmit(submitter) accepted any element and submitted
silently. Per the HTML spec (and Chrome) it must throw a TypeError when the
submitter is not a submit button, and a NotFoundError DOMException when the
submitter is not owned by the form; both checks run before the submit event.

Adds an _isSubmitButton predicate (a <button> whose type is submit — the
default, including a missing/invalid type — or an <input> of type submit/image)
and validates the argument at the top of requestSubmit. submit() is untouched:
it correctly bypasses the event (#395).

Element.click() now gates on the same predicate rather than its own looser
check, so an internal click can never hand requestSubmit a submitter it would
reject (e.g. <div type="submit">, which is not a submit button and no longer
submits — matching Chrome). That also aligns click() with the CDP click path in
input.rs, which already treats <input type=image> as a submit button. The
input.rs call sites are already guarded by equivalent conditions and pass no
submitter on the Enter-key path, so they are unaffected.

Constraint validation is deliberately out of scope: obscura has no validity
engine (checkValidity is a stub returning true), so wiring it here would be a
no-op. Noted in #424 as separate work.

Full obscura-cdp suite (72) green.
This commit is contained in:
Andrey Mnatsakanov authored and GitHub committed 2026-07-16 12:42:42 +02:00
1 parent 8698afc065
commit ccfa5fb87b
2 files changed
+100 -2

No files matched your search

+32 -2
View File
@@ -1150,6 +1150,17 @@ function _htmlAttrName(el, n) {
return n;
}
// A submit button per the HTML spec: a <button> whose type is submit — the
// default, including when the type attribute is missing or invalid — or an
// <input> of type submit/image. Used to validate requestSubmit's submitter.
function _isSubmitButton(el) {
if (!el || typeof el.localName !== "string") return false;
const type = ((el.getAttribute && el.getAttribute("type")) || "").toLowerCase();
if (el.localName === "button") return type !== "reset" && type !== "button";
if (el.localName === "input") return type === "submit" || type === "image";
return false;
}
class Element extends Node {
constructor(nid) {
super(nid);
@@ -1501,8 +1512,11 @@ class Element extends Node {
return;
}
}
const type = (this.getAttribute('type') || '').toLowerCase();
if (type === 'submit' || (this.localName === 'button' && type !== 'button' && type !== 'reset')) {
// Same predicate requestSubmit validates against, so an internal click
// can never hand it a submitter it would reject. Also matches the CDP
// click path in input.rs, which already treats <input type=image> as a
// submit button.
if (_isSubmitButton(this)) {
const form = this.closest ? this.closest('form') : null;
// A real submit-button click fires the cancelable submit event, so use
// requestSubmit() (not the plain submit() method, which now bypasses it).
@@ -1975,6 +1989,22 @@ class Element extends Node {
this._navigateSubmit(submitter);
}
requestSubmit(submitter) {
// Per spec, a given submitter must be a submit button owned by this form;
// both checks run before the submit event fires. A missing/null submitter
// means "submit from the form itself".
if (submitter !== undefined && submitter !== null) {
if (!_isSubmitButton(submitter)) {
throw new TypeError(
"Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not a submit button."
);
}
if (submitter.form !== this) {
throw new DOMException(
"Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not owned by this form element.",
'NotFoundError'
);
}
}
const cancelled = !this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
if (cancelled) return;
this._navigateSubmit(submitter);