Skip to content

fix(webapp): close js/xss on the framework-control deep link - #91

Merged
emmanuelgjr merged 1 commit into
mainfrom
fix/xss-control-detail-route
Sep 14, 2026
Merged

emmanuelgjr merged 1 commit into
mainfrom
fix/xss-control-detail-route

Conversation

@emmanuelgjr

Copy link
Copy Markdown
Contributor

What

Closes a DOM XSS (CodeQL js/xss class) on the framework-control deep link in docs/index.html, the hand-maintained single-page app (scripts/generate.js only writes the docs/*.js data bundles, so this edit is not regenerated away).

Why: the taint flow

window.location.hash                         getRoute()
  -> path
  -> fwControlMatch = path.match(/^\/frameworks\/([^/]+)\/(.+)$/)
  -> decodeURIComponent(match[1] | match[2])   fwNameCtrl, ctrlId
  -> renderControlDetail(app, fwNameCtrl, ctrlId)
  -> el(..., fwName | controlId)
  -> node.appendChild(c)                        <- sink

The sibling frameworkMatch route immediately below already allow-lists its input with FRAMEWORKS.indexOf(fwName) !== -1. The fwControlMatch branch had no equivalent.

No code-scanning analysis exists on this repo yet (code-scanning/alerts returns no analysis found), so there are no alert numbers to reference here; the same flow was flagged as alerts #3 and #4 in the monorepo.

Changes

  1. resolveControlId(fwName, controlId) looks the control id up in the same three places renderControlDetail reads (framework registry, backlink index, DATA mappings) and returns the stored string. The route checks the framework name against FRAMEWORKS, then renders only the resolved values, never URL text. A link that resolves to nothing falls through to the frameworks index. Adapted for this repo: the registry and backlink lookups use hasOwnProperty, so prototype keys can never match.
  2. el() only appends real nodes. Children go through toNode(), which returns a DOM node unchanged and wraps anything else in a text node. Before, a string inside an array made appendChild throw.

An exact allow-list from our own data is used instead of a character filter. Control ids are full of punctuation (GV-1.7, Art. 24-27, A.5.1), and CodeQL does not treat a negated character-class check as a sanitizer.

Verification

  • Inline <script> parses with vm.Script: 0 syntax errors.
  • Every deep link the app builds still resolves to itself (checked with the route code taken from the edited file, run against the real bundles):
Source Unique links Broken
backlinks.js 1159 0
frameworks-registry.js controls (incl. parent/child links) 1514 0
data.js mappings 1159 0
classifier-predictions.js (review page) 199 0
  • Payloads resolve to null and fall through, whether used as the control id, the framework name, or both, encoded or raw: <img src=x onerror=alert(1)>, "><svg/onload=alert(1)>, <script>alert(1)</script>, __proto__, constructor. 25 checks, 0 resolved.
  • el() tested with a DOM shim, 7/7 pass: a string child becomes textContent; an array holding a string gives a text node and no longer throws; element identity is kept; falsy entries are skipped; a number becomes a text node.
  • Repo checks give the same results on this branch and on main:
    • node scripts/validate.js: 0 errors, 84 warnings, 312 passed.
    • npm run stats:check: current.
    • node --test scripts/*.test.mjs: 50/50.
    • node scripts/generate.js: the only drift is the // Generated: date header.

Ported from GenAI-Security-Project/GenAI-Data-Security-Initiative#63 (that copy of crosswalk/ has since been removed)

🤖 Generated with Claude Code

https://claude.ai/code/session_01KJVKF6raJCNKBWjTkDSqR4

Port of GenAI-Security-Project/GenAI-Data-Security-Initiative#63 (squash
17f37f5), which fixed the same code in that repo's former crosswalk/ copy.

The #/frameworks/<fw>/<control> hash route (fwControlMatch) passed
URL-decoded text straight into renderControlDetail, which hands it to el()
and on to appendChild. The sibling frameworkMatch route already
allow-lists its input against FRAMEWORKS; this one did not.

- resolveControlId() resolves the control id against the same three
  sources renderControlDetail reads (framework registry, backlink index,
  DATA mappings) and returns the stored string. The route checks the
  framework name against FRAMEWORKS and renders only the resolved values,
  never URL text. Unresolved links fall through to the frameworks index.
  Map lookups use hasOwnProperty so prototype keys never match.
- el() routes children through toNode(), so anything that is not a DOM
  node is appended as a text node and can never be parsed as markup.

Verified: every deep link the app builds from backlinks.js (1159),
frameworks-registry.js (1514), data.js mappings (1159) and
classifier-predictions.js (199) still resolves to itself; XSS payloads
and __proto__/constructor resolve to null; validate, generate (no drift
beyond date headers), stats:check and 50/50 unit tests match main.

Co-authored-by: emmanuelgjr <129134995+emmanuelgjr@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJVKF6raJCNKBWjTkDSqR4
@emmanuelgjr
emmanuelgjr merged commit 99620eb into main Sep 14, 2026
2 checks passed
@emmanuelgjr
emmanuelgjr deleted the fix/xss-control-detail-route branch September 14, 2026 19:39
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Conflicts were only in the four generated webapp bundles, and only in
their header lines: #92 regenerated them with a `// Generated:` date
that this branch removes. Resolved by re-running scripts/generate.js on
the merged sources; a second run is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
No conflicts. Generated files re-run on the merged sources; a second
generation is byte-identical. validate 0 errors, 60/60 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Generated files re-run on the merged sources; a second generation is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Generated files re-run on the merged sources; a second generation is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Generated files re-run on the merged sources; a second generation is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Generated files re-run on the merged sources; a second generation is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 14, 2026
Re-key Otto Sulin's LLM Top 10 x AISVS mapping from the 2025 list to the
2026 list per MIGRATION.md: sections renumbered and renamed, cross-
references renumbered, severities re-baselined. Add eight requirements
for the 2026 scope changes: LLM01 cross-modal (2.2.3, 2.2.4), LLM04
artifact provenance (3.1.1, 3.1.3), LLM05 fine-tuning subversion (6.1.2,
3.5.1), LLM08 hidden context (10.2.4), LLM10 generated code (9.3.7).

Merge main (#91, #92) and regenerate entries, backlinks, bundles and
stats: 3,803 mappings, 77 mapping files, 26 frameworks.

Co-authored-by: Otto Sulin <ottosulin@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJVKF6raJCNKBWjTkDSqR4
emmanuelgjr added a commit that referenced this pull request Sep 15, 2026
Generated files re-run on the merged sources; a second generation is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
emmanuelgjr added a commit that referenced this pull request Sep 15, 2026
No conflicts. The one intermittent local test failure seen during verification is the pre-existing data/entries read/write race (Unexpected end of JSON input), fixed by #87; captured and confirmed, not caused by this merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR
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