User content does not reach dangerous sinks unsanitized
Why it matters
A handful of JavaScript APIs parse strings as live code or live HTML — dangerouslySetInnerHTML, eval(...), new Function(...), document.write(...), and direct element.innerHTML = ... assignment. Each one turns a string into executable behavior. When the string originates from user-controllable input — a request body, URL parameter, cookie, or database row that was itself populated from user input — and reaches the sink without sanitization, the result is stored or reflected XSS, arbitrary script execution, and in server contexts potential RCE via new Function(). The 2018 British Airways Magecart attack skimmed 380,000 customer payment cards through exactly this pattern: an injected script reached a rendering sink that trusted its input. The Information Commissioner's Office fined BA £20 million under GDPR Article 32. OWASP ranks Injection as the #3 web risk in its 2021 Top 10. AI coding tools produce this pattern whenever they scaffold a "rich text preview", a "markdown renderer", or a "dynamic expression evaluator" without wiring in a sanitizer — the happy path works, the attack path is invisible until exploited.
Severity rationale
Critical because a single path from user input to a live-HTML or live-code sink produces client-side XSS (session-cookie theft, account takeover) or server-side RCE — the full Magecart / supply-chain-compromise playbook in a single unsanitized line.
Remediation
Route every user-sourced string through a sanitizer before it reaches a dangerous sink:
import DOMPurify from 'isomorphic-dompurify';
export function UserPost({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Never pass user input to eval(...) or new Function(...) at all — those APIs have no safe usage in user-facing code. For expression-evaluator use cases, use a sandboxed library like expr-eval or jsep that parses to an AST rather than eval'ing strings. For a deeper XSS-audit pass including CSP nonce integration, DOMPurify version pinning, and dependency-graph sink discovery, run the security-hardening and ai-slop-security-theater Pro audits.
Detection
- ID:
dangerous-sinks-not-fed-user-content - Severity:
critical - What to look for: Grep
.{tsx,jsx,ts,js}(excludingnode_modules,dist,build,.next) for:dangerouslySetInnerHTML,eval(,new Function(,document.write(, regex\.innerHTML\s*=. For each match, trace whether the value originates (within 2 import hops) from a tainted source:req.body.*,req.json(),req.formData(),params.*,searchParams.*,useSearchParams().get(...),cookies().get(...),formData.get(...), Prisma/Supabase rows populated by user input. If the value passes through a library-grade sanitizer (DOMPurify.sanitize,sanitize-html,xss,isomorphic-dompurify) before the sink, it's safe. - Pass criteria: Zero dangerous-sink occurrences receive user-controllable content without a sanitizer between source and sink.
- Fail criteria: Any dangerous-sink usage traces back to a user-controllable source within 2 import hops without a sanitizer. "Sanitized" via a custom regex replace like
.replace(/<script>/g, '')does NOT count — regex-based HTML sanitization is defeated by malformed tags, nested encoding, SVG event handlers, and dozens of known bypasses. Only library-grade sanitizers count. - Skip (N/A) when: Project renders zero HTML and evaluates zero expressions (pure JSON API, CLI tool, library). Quote the absence of JSX / template files /
innerHTML/evalusage. - Before evaluating, quote: Each flagged occurrence + provenance chain showing how the value reached the sink.
- Report even on pass:
"3 dangerouslySetInnerHTML calls, all passing static markdown compiled at build time via remark-html; 0 from user sources." - Detail on fail:
"src/app/post/[id]/page.tsx:42 — dangerouslySetInnerHTML={{ __html: post.body }} where post.body is a Prisma row populated from req.body.content on POST /api/posts with no sanitizer". - Remediation: Library-grade sanitizer between every user source and every dangerous sink. For
eval/new Functionwith user input: replace with an AST-based parser or lookup-table evaluator — there is no safe usage.
External references
- cwe · CWE-79 — Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- cwe · CWE-95 — Improper Neutralization of Directives in Dynamically Evaluated Code (Eval Injection)
- cwe · CWE-94 — Improper Control of Generation of Code (Code Injection)
- owasp:2021 · A03 — Injection
Taxons
History
- 2026-04-23·v1.0.0·Initial Phase 9.1 v3.1 Stack Scan promotion — dangerous-sink-to-user-input paths produce stored XSS and RCE in AI-scaffolded rich-text and dynamic-expression UIs.·by phase-9-1-stack-scan-v3-1