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

@@ -182,3 +182,71 @@ async fn cdp_click_submit_button_is_vetoed_by_prevent_default_listener() {
"a CDP click on a submit button must fire the cancelable submit event and be vetoable"
);
}
// requestSubmit(submitter) must validate its argument before doing anything
// else (issue #424): a TypeError if the submitter is not a submit button, and a
// NotFoundError DOMException if it is not owned by the form. obscura accepted
// anything and silently submitted. The form under test preventDefault()s its
// own submit event so the valid-submitter case cannot navigate away.
#[tokio::test(flavor = "current_thread")]
async fn request_submit_validates_its_submitter_argument() {
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
let url = serve_form().await;
let mut ctx = CdpContext::new();
let page_id = ctx.create_page();
let session_id = "session-4";
ctx.sessions.insert(session_id.to_string(), page_id.clone());
navigate(&mut ctx, &url, session_id).await;
let v = cdp(
&mut ctx,
2,
"Runtime.evaluate",
json!({
"expression": r#"(() => {
document.body.innerHTML =
'<form id="vf">' +
'<div id="d"></div>' +
'<button id="ok" type="submit">go</button>' +
'<button id="plain" type="button">x</button>' +
'<input id="inp" type="text">' +
'</form>' +
'<button id="outside" type="submit">y</button>';
const f = document.getElementById('vf');
f.addEventListener('submit', (e) => e.preventDefault());
const probe = (fn) => {
try { fn(); return "no-throw"; }
catch (e) {
return (e instanceof DOMException) ? "DOMException:" + e.name
: (e && e.constructor && e.constructor.name) || String(e);
}
};
return JSON.stringify({
div: probe(() => f.requestSubmit(document.getElementById('d'))),
plain: probe(() => f.requestSubmit(document.getElementById('plain'))),
text: probe(() => f.requestSubmit(document.getElementById('inp'))),
outside: probe(() => f.requestSubmit(document.getElementById('outside'))),
valid: probe(() => f.requestSubmit(document.getElementById('ok'))),
noArg: probe(() => f.requestSubmit()),
nullArg: probe(() => f.requestSubmit(null)),
});
})()"#,
"returnByValue": true
}),
session_id,
)
.await;
let val = serde_json::from_str::<Value>(v["result"]["value"].as_str().unwrap()).unwrap();
assert_eq!(val["div"], "TypeError", "a non-button submitter must throw TypeError");
assert_eq!(val["plain"], "TypeError", "type=button is not a submit button");
assert_eq!(val["text"], "TypeError", "input type=text is not a submit button");
assert_eq!(
val["outside"], "DOMException:NotFoundError",
"a submit button not owned by the form must throw NotFoundError"
);
assert_eq!(val["valid"], "no-throw", "the form's own submit button must be accepted");
assert_eq!(val["noArg"], "no-throw", "requestSubmit() with no submitter is valid");
assert_eq!(val["nullArg"], "no-throw", "requestSubmit(null) means submit from the form itself");
}