diff --git a/crates/obscura-cdp/tests/form_submit_method_bypasses_listener.rs b/crates/obscura-cdp/tests/form_submit_method_bypasses_listener.rs index 778e525..5f068b6 100644 --- a/crates/obscura-cdp/tests/form_submit_method_bypasses_listener.rs +++ b/crates/obscura-cdp/tests/form_submit_method_bypasses_listener.rs @@ -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 = + '
' + + '
' + + '' + + '' + + '' + + '
' + + ''; + 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::(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"); +} diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 4009bed..b0d37cf 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -1150,6 +1150,17 @@ function _htmlAttrName(el, n) { return n; } +// A submit button per the HTML spec: a