Cross-Site Scripting happens when attacker-controlled content is rendered as code instead of as text. The browser executes it inside your origin, with full access to cookies, the DOM, and any API the user is logged into. Famous, fixable, and still everywhere — because every framework that escapes by default has at least one bypass developers reach for when they want raw HTML.
← Back to Security<script>steal()</script> in a comment field that gets shown back unescaped.location.hash and writes it into the DOM).The dangerous one. Attacker posts a comment, a profile bio, a product review containing a script. The server stores it; every subsequent viewer's browser executes it. One injection, many victims, no link to click.
Targets: any field that's saved and shown to others — comments, names, bios, support tickets, chat messages, custom fields.
Input from one request is echoed back in the response. The classic vector is an error page or search result that renders the query: /search?q=<script>.... The attacker emails or links the URL; the victim clicks; the script runs in the victim's session.
The server is innocent — the page's own JavaScript reads location.hash, document.referrer, or another attacker-controllable source and writes it into the DOM via innerHTML, document.write, or eval. Server-side defenses don't help; the bug lives in the client.
Audit for: sinks like el.innerHTML = userInput, $().html(userInput), eval(), new Function(), and dangerous React/Angular escapes (dangerouslySetInnerHTML, bypassSecurityTrustHtml).
The single most effective defense. The encoding required depends on where the value lands:
< > & " as entities.<script type="application/json"> tag and parse from JS — don't interpolate into a JS literal.javascript:).Modern frameworks (React, Angular, Vue, Svelte, Razor, Thymeleaf, Django templates) escape by default. Use them. Audit any place you opt out (dangerouslySetInnerHTML, v-html, {!...}).
A response header that tells the browser which scripts it's allowed to run. A strict CSP turns most XSS attempts into nothing — the browser refuses to execute injected scripts because they don't match an allow-listed source or nonce.
Modern recipe: nonce-based CSP — every legitimate inline script gets a per-request nonce; injected scripts have no nonce; the browser refuses them.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-aB9c...' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; report-uri /csp-report;
Roll out in report-only mode first. CSP misconfigurations are the kind of thing that ships fine and breaks the home page on a Tuesday.
If a feature genuinely needs rich text (rich text editors, markdown that allows HTML), use a tested sanitizer — DOMPurify in the browser, OWASP Java HTML Sanitizer, bleach for Python — with a strict allow-list of tags and attributes. Never write your own HTML sanitizer; the edge cases are endless.
<a href="userInput"> is XSS if userInput is javascript:alert(1). Allow-list http:, https:, mailto:, and reject everything else. Same for src on img, iframe, script.
HttpOnly cookies are invisible to JS — even successful XSS can't steal the session token via document.cookie. Secure ensures TLS-only. SameSite=Lax reduces CSRF (a different bug, but related). These don't prevent XSS; they reduce the blast radius when one slips through.
A browser feature (Chromium-based) that makes DOM XSS sinks (innerHTML, eval) refuse to accept plain strings — only special "trusted" objects produced by your sanitizer. Catches DOM XSS by construction. Worth enabling on new apps.
React, Vue, Angular, Svelte, and modern templating engines escape by default for the HTML body context. That kills the most common stored/reflected XSS. The remaining risks are:
dangerouslySetInnerHTML, v-html, {!...}, raw template helpers. Search the codebase regularly.href or style needs URL/CSS-aware sanitizing.location, postMessage, third-party scripts.