Merge pull request #2549 from lightpanda-io/recursive_submit

Protect against recursive form submits
This commit is contained in:
Karl Seguin
2026-05-26 21:58:40 +08:00
committed by GitHub
4 changed files with 92 additions and 1 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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, .{});