Skip to content

rules(js): add balanced innerHTML/outerHTML DOM XSS rule - #65

Draft
asadeddin wants to merge 2 commits into
mainfrom
cursor/js-ts-innerhtml-xss-rule-43a0
Draft

rules(js): add balanced innerHTML/outerHTML DOM XSS rule#65
asadeddin wants to merge 2 commits into
mainfrom
cursor/js-ts-innerhtml-xss-rule-43a0

Conversation

@asadeddin

Copy link
Copy Markdown
Contributor

What

Adds js-dom-xss-innerhtml-001 to rules/javascript/frontend_security.ron: a search-mode rule for JS/TS (.js, .jsx, .ts, .tsx) that flags innerHTML / outerHTML assignments the scanner cannot prove are static HTML.

The only existing search-mode innerHTML coverage was js-dom-xss-001, which keys off identifier names (*user*, *input*, *data*, *param*), so it misses the common shapes of the bug and fires on unrelated code that happens to contain the word "data". The new rule is left after it, so where both match, findings are deduplicated on (line, "DOM XSS") and the existing rule keeps precedence — the change is purely additive.

What it catches

Three regex patterns, each anchored with ^[^=;{]* so the match stays on the assignment that starts the matched node (before anchoring, an enclosing forEach(... => { ... }) block was reported instead of the sink line):

  1. Interpolated template literal — el.innerHTML = \${value}`(including multi-line templates containing=` in attributes)
  2. Markup concatenated with an expression — el.innerHTML = '<b>' + value, el.innerHTML = value + '</b>'
  3. Non-literal value — el.innerHTML = value, el.innerHTML = render(value), el.innerHTML += value, and the TypeScript cast form (el.innerHTML as string | TrustedHTML) = ...

What stays quiet

el.innerHTML = '', static string/template literals, literal-only concatenation ('<span>' + 'static' + '</span>'), reads (const html = el.innerHTML), comparisons (===, !==), textContent/className/setAttribute, and same-node sanitizer calls (DOMPurify.sanitize, escapeHTML, sanitizeHtml, xss, …) via the engine's existing XSS sanitization check. JSX dangerouslySetInnerHTML is untouched and stays with its own rule.

Severity is High, confidence Medium: search mode is line-local, so sanitization performed on an earlier line (const clean = DOMPurify.sanitize(x); el.innerHTML = clean;) is not visible to it — the flow-aware case is what the taint rules cover.

Second commit

check_html_sanitization accepted escapeHtml( but not the escapeHTML( casing that the rule files themselves list as a sanitizer, and omitted sanitizeHtml( (the sanitize-html package) entirely, so el.innerHTML = escapeHTML(v) was reported. Both casings plus sanitizeHtml(/sanitizeHTML( are now recognized, which applies to every JS/TS XSS/DOM search rule.

Validation

  • make check (201 tests) and make pre-push (clippy, format, cucumber acceptance) pass. make complexity and make arch could not run in this environment (uvx and cargo-modules are not installed); the change adds no branching or module structure.
  • Scanned probe files plus the repo's JS fixtures under tests/test_files: 23 new findings, all on intentionally vulnerable lines, and no previously reported finding was lost. Two known trade-offs remain visible there: coercions such as el.innerHTML = String(count) are flagged, and values sanitized on a preceding line (xss_comprehensive_test.js:171, :219) are flagged — both inherent to line-local matching, hence Medium confidence.

Note: the workspace has no corgea MCP server available, so the vulnerability-class research for this rule was done from the repo's own rules, fixtures, and scanner semantics.

Open in Web Open in Cursor 

cursoragent and others added 2 commits July 30, 2026 17:58
The only innerHTML coverage in search mode was js-dom-xss-001, which keys off
identifier names (*user*, *input*, *data*, *param*) and therefore misses the
common shapes of the bug: template-literal interpolation, markup concatenated
with an expression, and plain assignment of a variable or call result.

The new rule matches those three shapes with anchored regexes so a match stays
on the assignment that starts the node (enclosing blocks are no longer reported
in place of the sink line), while static literals, reads and comparisons stay
quiet. Sanitized values are dropped by the existing XSS sanitization check.
Confidence is Medium because search mode cannot see sanitization that happened
on an earlier line.

Co-authored-by: Ahmad Sadeddin <asadeddin@users.noreply.github.com>
The JS/TS sanitizer list accepted escapeHtml( but not the escapeHTML( casing the
rule files themselves list, and omitted sanitizeHtml( (sanitize-html) entirely,
so `el.innerHTML = escapeHTML(v)` was reported as XSS.

Co-authored-by: Ahmad Sadeddin <asadeddin@users.noreply.github.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.

2 participants