flawopen.com/xss/Javascript
JavaScript XSS: Secure DOM, React and Express
Unsafe DOM/React sinks and links can run injected JavaScript. Prefer text APIs; sanitize HTML, validate URLs and enforce Trusted Types (CWE-79).
Think of a theater cue board. If a stage manager treats every guest note as stage directions, a note can add an unexpected scene; if the note is displayed as ordinary dialogue, it stays text. In a browser, innerHTML and React’s dangerouslySetInnerHTML ask the DOM to parse supplied text as markup, while textContent displays it literally. Escape normal text, sanitize the small amount of HTML you intentionally allow, and validate URL schemes before creating links.
Core Concepts & Subsystem Terms
innerHTML vs textContent- Assigning to
innerHTML,outerHTMLorinsertAdjacentHTMLmakes the browser parse the string as HTML, so an<img onerror>in it becomes a live element.textContentandcreateTextNodeinsert text only; nothing in the value is ever parsed as markup. Template literals in res.send()- Express's
res.send()sends a string astext/htmlby default. A template literal evaluates${req.query.q}before sending, so the response contains whatever the user put in the query string.req.query.qcan also be an array or object, so check its type. dangerouslySetInnerHTML + DOMPurify- React escapes text in JSX automatically;
dangerouslySetInnerHTMLis the explicit opt-out. Run the value throughDOMPurify.sanitize(), which parses it with the browser's own parser and removes scripts, event handlers and dangerous URLs before React inserts it. javascript: URLs in href- JSX escapes the attribute value, but
href="javascript:..."is a valid URL that runs code when clicked. Parse withnew URL()and allow onlyhttp:andhttps:; the URL parser strips leading spaces and tabs that simple prefix checks miss. Trusted Types- With the CSP directive
require-trusted-types-for 'script', supported browsers refuse plain strings in sinks such asinnerHTMLand accept onlyTrustedHTMLobjects created by a policy you define, typically one that calls DOMPurify. New sinks cannot slip in unnoticed.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Input reaches a sink
A search query, a profile field loaded from an API, or a link stored by another user arrives as a string and is passed to innerHTML, dangerouslySetInnerHTML, href or res.send().
The value becomes markup
With the benign test value <img src=x onerror=alert(1)>, innerHTML and res.send() produce a real <img> element; a stored javascript:alert(1) link becomes a clickable script.
The browser runs it
The image fails to load and fires onerror, or the user clicks the link. The code runs immediately, with no further action from the attacker.
The script acts as the site
It runs with the page's origin: it can read the DOM, call the site's APIs with the victim's cookies or tokens from localStorage, and change what the user sees.
Source Code: Flaw vs. Secure Implementation
// profile.jsx: each function below lets a string be parsed as HTML
import express from 'express';
const app = express();
// Express: the query value is placed in the HTML before sending
app.get('/search', (req, res) => {
res.send(`<h1>Results for ${req.query.q}</h1>`);
});
// innerHTML parses name as markup
export function showGreeting(el, name) {
el.innerHTML = `Hello, ${name}`;
}
// React inserts the HTML exactly as received
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}
// a javascript: URL runs when the link is clicked
export function Website({ url }) {
return <a href={url}>Website</a>;
}
// profile.jsx: text stays text; HTML and URLs are checked first
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';
const app = express();
// only strings are accepted, and they are HTML-escaped
app.get('/search', (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q : '';
res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});
// textContent never parses markup
export function showGreeting(el, name) {
el.textContent = `Hello, ${name}`;
}
// DOMPurify removes scripts and event handlers before insertion
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// only http and https URLs survive; everything else becomes #
function safeHref(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
} catch {
return '#';
}
}
export function Website({ url }) {
return <a href={safeHref(url)}>Website</a>;
}
Engineering & System Hardening Checklist
- ✓Enable
react/no-dangerandeslint-plugin-no-unsanitizedin CI, and blockinnerHTML,outerHTML,insertAdjacentHTMLanddocument.writewith non-literal values. - ✓Use
textContentfor text; pass any HTML that must be rendered throughDOMPurify.sanitize()at the point of insertion. - ✓Validate every user-supplied URL with
new URL()and anhttp:/https:allowlist before it reacheshref,srcorwindow.location. - ✓In Express, render through an auto-escaping view engine or
escape-html, and reject non-string query parameters. - ✓Send a CSP with a nonce-based
script-src, no'unsafe-inline', andrequire-trusted-types-for 'script'; keep session tokens inHttpOnlycookies rather thanlocalStorage.