mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 17:46:32 -04:00
Merge pull request #2549 from lightpanda-io/recursive_submit
Protect against recursive form submits
This commit is contained in:
@@ -4046,6 +4046,11 @@ const SubmitFormOpts = struct {
|
||||
pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.Form, submit_opts: SubmitFormOpts) !void {
|
||||
const form = form_ orelse return;
|
||||
|
||||
// see the `_constructing_entry_list` field documentation
|
||||
if (form._constructing_entry_list) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitter_) |submitter| {
|
||||
if (submitter.getAttributeSafe(comptime .wrap("disabled")) != null) {
|
||||
return;
|
||||
@@ -4083,6 +4088,14 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
|
||||
};
|
||||
|
||||
if (submit_opts.fire_event) {
|
||||
// Prevent a submit on the form from firing while we're submit the form.
|
||||
// This is both spec-correct AND prevents infinite recursion.
|
||||
if (form._firing_submission_events) {
|
||||
return;
|
||||
}
|
||||
form._firing_submission_events = true;
|
||||
defer form._firing_submission_events = false;
|
||||
|
||||
// Per HTML spec "submit a form element" algorithm: SubmitEvent.submitter
|
||||
// must be null when the submitter is the form itself, which is what
|
||||
// Form.requestSubmit() passes when called with no submitter argument.
|
||||
|
||||
@@ -561,3 +561,65 @@
|
||||
testing.expectEqual('big5', form.getAttribute('accept-charset'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Test: re-entrant submission from a `submit` handler is a no-op. Without the
|
||||
"firing submission events" guard this recurses unbounded and fatally aborts
|
||||
the V8 isolate. -->
|
||||
<form id="test_form_reentrant_submit" action="/should-not-navigate8" method="get">
|
||||
<input type="text" name="q" value="test">
|
||||
</form>
|
||||
|
||||
<script id="submit_reentrant_from_onsubmit_is_noop">
|
||||
{
|
||||
const form = $('#test_form_reentrant_submit');
|
||||
let count = 0;
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
count++;
|
||||
// Re-entrant submit while submission events are firing must be ignored.
|
||||
form.requestSubmit();
|
||||
});
|
||||
|
||||
form.requestSubmit();
|
||||
testing.expectEqual(1, count);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Test: re-entrant submission from a `formdata` handler is a no-op. Without
|
||||
the "constructing entry list" guard this recurses unbounded. -->
|
||||
<script id="submit_reentrant_from_formdata_is_noop">
|
||||
{
|
||||
const form = document.createElement('form');
|
||||
let count = 0;
|
||||
|
||||
form.addEventListener('formdata', () => {
|
||||
count++;
|
||||
// form.submit() while the entry list is being constructed must be ignored.
|
||||
form.submit();
|
||||
});
|
||||
|
||||
new FormData(form);
|
||||
testing.expectEqual(1, count);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Test: building another FormData from a `formdata` handler throws
|
||||
InvalidStateError (and is guarded against unbounded recursion). -->
|
||||
<script id="formdata_reentrant_construction_throws">
|
||||
{
|
||||
const form = document.createElement('form');
|
||||
let caught = null;
|
||||
|
||||
form.addEventListener('formdata', () => {
|
||||
try {
|
||||
new FormData(form);
|
||||
} catch (e) {
|
||||
caught = e.name;
|
||||
}
|
||||
});
|
||||
|
||||
new FormData(form);
|
||||
testing.expectEqual('InvalidStateError', caught);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,14 @@ pub const TextArea = @import("TextArea.zig");
|
||||
const Form = @This();
|
||||
_proto: *HtmlElement,
|
||||
|
||||
// Prevents submission of the form while we're in the process of submitting
|
||||
// the form. You can imagine an onsubmit = () => form.submit() endless loop.
|
||||
_firing_submission_events: bool = false,
|
||||
|
||||
// Prevents submission of the form while we're building the entry list for the
|
||||
// form. You can imagine an formdata = () => form.submit() endless loop.
|
||||
_constructing_entry_list: bool = false,
|
||||
|
||||
pub fn asHtmlElement(self: *Form) *HtmlElement {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,14 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD
|
||||
.worker => lp.assert(false, "FormData worker form", .{}),
|
||||
};
|
||||
|
||||
if (form._constructing_entry_list) {
|
||||
// see the `_constructing_entry_list` field documentation
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
form._constructing_entry_list = true;
|
||||
defer form._constructing_entry_list = false;
|
||||
|
||||
const form_data = try exec._factory.create(FormData{
|
||||
._arena = exec.arena,
|
||||
._entries = try collectForm(frame.arena, form, submitter, frame),
|
||||
@@ -397,7 +405,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(FormData.init, .{});
|
||||
pub const constructor = bridge.constructor(FormData.init, .{ .dom_exception = true });
|
||||
pub const has = bridge.function(FormData.has, .{});
|
||||
pub const get = bridge.function(FormData.get, .{});
|
||||
pub const set = bridge.function(FormData.set, .{});
|
||||
|
||||
Reference in New Issue
Block a user