Skip to content

fix(security): close M-1 — DOM construction instead of innerHTML in _renderTestResults#52

Merged
keyxmakerx merged 2 commits into
mainfrom
claude/fm-sec-chunk-1
May 23, 2026
Merged

fix(security): close M-1 — DOM construction instead of innerHTML in _renderTestResults#52
keyxmakerx merged 2 commits into
mainfrom
claude/fm-sec-chunk-1

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Cites: 2026-05-21-core-tenets §T-B1; reports/foundry/2026-05-22-fm-security-audit.md §2 M-1, §4 Chunk 1, §0.5 D1=(c); dispatches/foundry/FM-SEC-CHUNK-1.md
Security implication: closes M-1 (latent XSS via innerHTML interpolation of Chronicle-side strings). Without this fix, a compromised Chronicle returning <img src=x onerror=alert(1)> as match.name would execute JavaScript in the dashboard's DOM context.
Consumer-verified: Chronicle returns entity/system names that get rendered in the test-connection result; verified Chronicle-side that these are operator-controllable per reports/foundry/2026-05-22-fm-security-audit.md §1.3.
Foundry compatibility: static-source code change only; renders identically across v12/v13/v14 (DOM APIs replaceChildren/createElement/createTextNode are ES standard, all Foundry-supported versions).
Mockup: n/a — internal admin surface; visually identical after fix.

What this changes

_renderTestResults in scripts/sync-dashboard.mjs now constructs the test-connection diagnostic via replaceChildren + createElement + createTextNode instead of innerHTML = ... interpolation. Chronicle-side strings render as text nodes, never as DOM. Static-source regression guard added at tools/test-sync-dashboard-xss.mjs.

Why

The Foundry-side security audit (reports/foundry/2026-05-22-fm-security-audit.md §2 M-1) flagged this site as a latent XSS surface. The audit's §0.5 D1=(c) resolution chose targeted fixes for the three medium findings; this is the first of those. Closes the XSS vector even in a trust-Chronicle-by-default deployment model.

Test plan

  • node --test tools/test-*.mjs passes locally (250 baseline + 4 new = 254/254)
  • node tools/check-package-descriptor.mjs passes (descriptor unchanged but ran for hygiene)
  • Brace balance verified
  • innerHTML site inventory: only the static spinner at sync-dashboard.mjs:1641 remains (a constant <i class="fa-solid fa-spinner"> literal with no interpolation — verified safe)
  • Manual verification in Foundry: launch a world; open the Sync Dashboard → Config tab; click "Test Connection"; verify the result list renders identically to pre-fix (icons + text)
  • Static-source regression guard pins the fix: re-introduction of innerHTML = ... inside _renderTestResults would fail the new test, as would adding any new innerHTML site to sync-dashboard.mjs outside the allowlist

Tenet self-check

  • T-B1 security: closes latent XSS vector via DOM construction
  • T-B2 plugin isolation: this module IS the foundry-vtt plugin half; changes stay within it
  • T-B3 production UI: visually identical; no transition/loading/error states changed
  • T-B4 dual-audience docs: code comment + the regression test's docstring explain the fix for both audiences

Stop-and-flag

None encountered. Two surfaces audited per §2 M-1:

  • sync-dashboard.mjs:1641 — static spinner; verified safe (no interpolation). Allowed by the regression guard.
  • sync-dashboard.mjs:1747 — the M-1 site; fixed.

Note on commit history

Two commits instead of one due to the chronicle-foundry-module-specific signing infra failure (HTTP 400 "missing source"; documented across PR #49, #50, #51 status reports + tracked in BACKLOG). Workaround: MCP push_files, which serializes large file contents per call. The test file landed first (small), then the modified sync-dashboard.mjs (large).

Squash-on-merge will collapse cleanly if desired.


🤖 Cites dispatches/foundry/FM-SEC-CHUNK-1.md. Generated by Claude Code.


Generated by Claude Code

Static-source guard that pins _renderTestResults' use of safe DOM
construction (textContent / createTextNode / replaceChildren) and
rejects re-introduction of innerHTML assignment inside that function.
Also flags any new innerHTML site in sync-dashboard.mjs outside the
existing static-spinner allowlist.

Same shape as test-sync-calendar-discoverability.mjs and
test-no-instance-hostname.mjs. Pair commit follows with the actual
_renderTestResults refactor.

Cites: 2026-05-21-core-tenets §T-B1
       reports/foundry/2026-05-22-fm-security-audit.md §2 M-1, §4 Chunk 1, §0.5 D1=(c)
`_renderTestResults` rendered Chronicle-side strings (system names,
error messages echoing Chronicle response data) via innerHTML
interpolation. If Chronicle returned a malicious string (e.g.,
`<img src=x onerror=alert(1)>` as `match.name`), the dashboard
would execute the payload as DOM.

Fix: replaceChildren + createElement + createTextNode. Icons use a
static class map (no interpolation). Chronicle strings rendered as
text nodes — never parsed as DOM.

Paired with tools/test-sync-dashboard-xss.mjs (committed previously
on this branch) — static-source guard against re-introduction of
innerHTML in _renderTestResults plus any new innerHTML site outside
the static-spinner allowlist.

Test suite: 250 baseline + 4 new = 254/254 pass.

Cites: 2026-05-21-core-tenets §T-B1
       reports/foundry/2026-05-22-fm-security-audit.md §2 M-1, §4 Chunk 1, §0.5 D1=(c)
@keyxmakerx keyxmakerx merged commit a8c5228 into main May 23, 2026
1 check passed
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