Skip to content

fix(forms): prevent stored XSS in admin submissions view and render it in the admin layout - #1100

Open
josevenceslau wants to merge 1 commit into
SonicJs-Org:mainfrom
josevenceslau:fix/admin-form-submissions-xss-and-layout
Open

josevenceslau wants to merge 1 commit into
SonicJs-Org:mainfrom
josevenceslau:fix/admin-form-submissions-xss-and-layout

Conversation

@josevenceslau

Copy link
Copy Markdown

Problem

GET /admin/forms/:id/submissions (packages/core/src/routes/admin-forms.ts) builds a bare HTML page and interpolates submission_data — public, user-supplied form input — straight into the page:

<td><pre>${JSON.stringify(JSON.parse(sub.submission_data), null, 2)}</pre></td>

This is an unescaped sink. A visitor can submit a form field containing markup/script (e.g. a message value of <img src=x onerror=…> or <script>…</script>), and it then executes in the admin's browser when they open the submissions list — a stored XSS in the admin UI, reachable by any anonymous form submitter. The page is also unstyled (a standalone <!DOCTYPE html> with inline CSS, no admin chrome), unlike every other admin forms page.

Fix

  • Moves the page into a dedicated template, admin-forms-submissions.template.ts, rendered inside the Catalyst admin layout via renderTable — consistent with admin-forms-list.template.ts and the rest of the admin.
  • Escapes every key and value of submission_data (and the form display name) through the existing escapeHtml util. Non-JSON submission_data falls back to an escaped raw string.
  • Surfaces the submission_number and status columns that were already stored but never displayed.

The route handler now just calls the template (net −34 lines).

Tests

src/__tests__/templates/admin-forms-submissions.test.ts (4 tests):

  • escaping of <script> / <img onerror> / attribute payloads in values, keys, and the form name;
  • the non-JSON submission_data fallback;
  • the empty state;
  • the chromed render (layout + heading + count).

tsc --noEmit and ESLint pass; no existing tests reference the old page.

Notes

No existing issue tracked this — happy to file one to link if the maintainers prefer. I have a follow-up (submission status workflow, delete, filters/search, audit) built on top of this in a downstream project; glad to open it as a separate feature PR if there's interest.

…t in the admin layout

The GET /admin/forms/:id/submissions handler built a bare HTML page and
interpolated submission_data — public, user-supplied form input — directly
into the page via `${JSON.stringify(JSON.parse(sub.submission_data))}`, an
unescaped sink. A visitor can submit a form field containing markup/script
that then executes in the admin's browser when they open the submissions
list (stored XSS in the admin UI).

Move the page into a dedicated template (admin-forms-submissions.template.ts)
that renders inside the Catalyst admin layout like the other forms pages, and
escape every key and value (and the form display name) via the existing
escapeHtml util. Also surfaces the submission number and status columns that
were already stored but not shown.

Adds tests covering the escaping (script/img/attribute payloads), the
non-JSON fallback, and the empty state.

This branch is waiting to be deployed

1 waiting deployment
external — 82c93503 Waiting Sep 16, 2026 by josevenceslau via authorize #1604
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant