Cross-site scripting (XSS): how it works and how to stop it
XSS is untrusted text written into a page as HTML. The browser cannot tell your script from one an attacker smuggled in, so it runs both — on your origin, with the visitor's session.
Example
app.get("/search", (req, res) => {
res.send("<h1>Results for " + req.query.q + "</h1>");
});
// client side
results.innerHTML = comment.body;import escapeHtml from "escape-html";
app.get("/search", (req, res) => {
res.send("<h1>Results for " + escapeHtml(String(req.query.q)) + "</h1>");
});
// client side
results.textContent = comment.body;
// or, when HTML is required: results.innerHTML = DOMPurify.sanitize(comment.body);How it happens
Reflected XSS sends a crafted link whose parameter is echoed into the response. Stored XSS saves the payload — a comment, a profile field — and serves it to everyone who views it. DOM-based XSS happens entirely in the browser, through innerHTML, document.write or insertAdjacentHTML. Templates with autoescaping switched off turn every rendered variable into a candidate.
How to fix it
Escape on output, and limit what an injected script could do.
- Render text as text: textContent, escaped template variables, React's default rendering
- Keep template autoescaping on; mark only sanitised values as safe
- Sanitise with DOMPurify when HTML is genuinely required
- Add a Content-Security-Policy without 'unsafe-inline', and HttpOnly session cookies
How RepoVerse finds it
- HighRequest input is written into the page as HTML
Script in the input runs in the victim's browser on your origin, with their session.
data flow · CWE-79
- HighRequest input is sent back inside an HTML response
A crafted link makes the response carry the attacker's script, which then runs as your site.
data flow · CWE-79
- HighTemplate autoescaping switched off
Every variable rendered into a template is emitted as raw HTML, so any user-supplied field shown on a page becomes stored or reflected XSS.
data flow · CWE-79
- ModerateMarkup written from a value
A variable is written into the DOM as HTML rather than as text.
pattern
- HighLink target taken from a value
An `href` or `src` is set from a variable.
pattern · CWE-79
- ModerateCSP allows inline scripts
`'unsafe-inline'` in `script-src` lets injected `<script>` tags and event handlers run, which is the main thing a CSP is there to stop.
website · CWE-79
Questions
- Does React prevent XSS?
- React escapes values rendered in JSX, which prevents most XSS. dangerouslySetInnerHTML, href values starting with javascript: and server-side string building bypass that protection.
- Is a Content-Security-Policy enough against XSS?
- It is a second layer: a strict CSP stops most injected scripts from running, but the injection is still a bug. Fix the output encoding and keep the CSP.
Related
- Content Security Policy: writing one that actually protectsWrite a Content-Security-Policy that actually stops XSS: why unsafe-inline, unsafe-eval and wildcard sources defeat it, nonces and strict-dynamic, and a policy to start from.
- Secure session cookies: the flags and the login stepSet session cookies safely: Secure, HttpOnly and SameSite flags (CWE-614, CWE-1004), regenerating the session at login against fixation, with Express examples.
- Find vulnerable JavaScript libraries on a live websiteFind outdated, vulnerable JavaScript libraries on any website — jQuery, lodash, Bootstrap, moment, DOMPurify, Next.js — even inside minified Vite and webpack bundles.
Updated 2026-10-11