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:
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"
|
"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");
|
||||||
|
}
|
||||||
Vendored
+32
-2
@@ -1150,6 +1150,17 @@ function _htmlAttrName(el, n) {
|
|||||||
return 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 {
|
class Element extends Node {
|
||||||
constructor(nid) {
|
constructor(nid) {
|
||||||
super(nid);
|
super(nid);
|
||||||
@@ -1501,8 +1512,11 @@ class Element extends Node {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const type = (this.getAttribute('type') || '').toLowerCase();
|
// Same predicate requestSubmit validates against, so an internal click
|
||||||
if (type === 'submit' || (this.localName === 'button' && type !== 'button' && type !== 'reset')) {
|
// 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;
|
const form = this.closest ? this.closest('form') : null;
|
||||||
// A real submit-button click fires the cancelable submit event, so use
|
// A real submit-button click fires the cancelable submit event, so use
|
||||||
// requestSubmit() (not the plain submit() method, which now bypasses it).
|
// requestSubmit() (not the plain submit() method, which now bypasses it).
|
||||||
@@ -1975,6 +1989,22 @@ class Element extends Node {
|
|||||||
this._navigateSubmit(submitter);
|
this._navigateSubmit(submitter);
|
||||||
}
|
}
|
||||||
requestSubmit(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 }));
|
const cancelled = !this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
this._navigateSubmit(submitter);
|
this._navigateSubmit(submitter);
|
||||||
|
|||||||
Reference in new issue
Block a user