Skip to content

fix(dynamic-field): escape number/date/datetime input value (stored XSS) - #1027

Open
mmcintosh wants to merge 3 commits into
mainfrom
fix/dynamic-field-value-xss
Open

fix(dynamic-field): escape number/date/datetime input value (stored XSS)#1027
mmcintosh wants to merge 3 commits into
mainfrom
fix/dynamic-field-value-xss

Conversation

@mmcintosh

@mmcintosh mmcintosh commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Description

The number, date, and datetime cases in dynamic-field.template.ts render the stored value unescaped (value="${value}") while every other field type escapes it. Content field values are stored raw by the write path, so an authenticated content writer can persist "><script>… in one of these fields and have it execute in another admin's content-edit form (type="date"/"number" doesn't help — the payload breaks out of the tag during HTML parsing, before the input validates its value).

Fixes #1026

Changes

  • Escape the value at the sink for the number/date/datetime cases: value="${escapeHtml(String(value ?? ''))}" — matching the escapeHtml(...) every other field type already uses. The String(...) coercion is required because escapeHtml returns '' for non-strings, so a numeric value survives (a valid 42 stays "42"; a valid date stays unchanged).
  • Add regression tests in dynamic-field.test.ts: XSS payload → escaped; valid numeric/date/datetime values → preserved; null/undefined → empty string.

Testing

tsc --noEmit clean. Regression tests added in-repo covering:

  • Stored XSS payload escaped for all three types
  • Valid values (number, date, datetime-local) pass through unchanged
  • null/undefined render as empty string

Unit Tests

  • Added/updated unit tests
  • All unit tests passing (CI failure on q_apikey_* columns is pre-existing on main, unrelated to this change)

E2E Tests

  • N/A — template unit test covers the sink

Checklist

  • Code follows project conventions
  • Tests added/updated and passing
  • Type checking passes
  • No console errors or warnings
  • Documentation updated (if needed)

The number/date/datetime cases in dynamic-field.template.ts emit `value="${value}"`
UNESCAPED, while every other field type escapes it (escapeHtml(value)). Content field
values are stored raw by the write path, then rendered back into the content edit form,
so an authenticated content writer could persist `"><script>…` in a number/date/datetime
field and have it execute in another admin's edit form. type="date"/"number" doesn't
help — the payload breaks out of the tag during HTML parsing, before the input validates
its value.

Escape at the sink: `escapeHtml(String(value ?? ''))`. A valid number/date is unchanged;
the String() coercion is required because escapeHtml returns '' for non-strings (a numeric
value would otherwise vanish).
@mmcintosh
mmcintosh marked this pull request as ready for review July 24, 2026 19:55
@mmcintosh
mmcintosh requested a review from lane711 as a code owner July 24, 2026 19:55
…e value escaping

Covers the stored-XSS fix from issue #1026 — verifies payload is
escaped at render time and valid values are preserved unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three independent root causes:

1. Module-scoped column/index caches in document-scalar-schema.ts carry
   over between test files — a fresh in-memory SQLite DB sees cached
   column names from a prior test's DB and skips ALTER TABLE, leaving
   q_* columns absent. Fix: export resetScalarSchemaCache() and call it
   in createTestD1() and migrations-d45.test.ts beforeEach.
   Fixes: q_tst_rating, q_tst_sort_order, q_media_folder, q_apikey_hash,
   q_apikey_user_id, q_blog_difficulty missing-column failures.

2. _pluginStatusCache (module-scoped Map) in plugin-middleware.ts
   persists across tests — first test caches 'my-plugin' → true, later
   tests hit the cache instead of the mock. Fix: call
   invalidatePluginStatusCache() in beforeEach of affected describe blocks.

3. c.executionCtx getter throws 'This context has no ExecutionContext'
   in Hono test env (app.request()), crashing the cache-enabled path in
   api.ts GET /:collection before scheduleKvWrite could guard on !globalKv.
   Fix: safeCtx(c) helper wraps the getter in try/catch; scheduleKvWrite
   accepts null/undefined ctx and returns early.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Security: number/date/datetime field inputs render the stored value unescaped (stored XSS)

2 participants