Cross-Site Scripting (XSS)
Cross-site scripting (XSS) is when an attacker gets a browser to run unintended JavaScript in the context of your site. Because modern apps render so much user-generated content, XSS is one of the most common web vulnerabilities — and a successful one can steal sessions, rewrite the page, or take over accounts.
The defense is conceptually simple: render untrusted data as text, not as HTML. Most XSS comes from a single place where that boundary was crossed.
TL;DR
- Prevent XSS primarily with output encoding and safe rendering.
- Validate input, but never rely on validation alone.
- Treat raw-HTML rendering (
innerHTML,dangerouslySetInnerHTML) as privileged. - If you must accept HTML, sanitize it with a well-reviewed library.
- Add a strong Content-Security-Policy as a backstop.
Quick Example
The vulnerability and its fix in one place — let the framework escape, or escape yourself:
Types of XSS
- Reflected — untrusted input is echoed straight back in the response (often via a query param) and executed.
- Stored — malicious content is saved (DB) and later served to many users (comments, profiles, wiki pages).
- DOM-based — the bug lives entirely in frontend JavaScript using unsafe DOM APIs, even when the server response is "safe."
Root Causes
XSS almost always comes from rendering untrusted strings as HTML, or feeding them to a dangerous sink:
element.innerHTML = ...document.write(...)eval(...)/new Function(...)- setting event-handler attributes (
onclick = ...) - framework "raw HTML" escapes (
dangerouslySetInnerHTML,v-html)
Prevention Strategies (in priority order)
1. Output encoding — the real fix
Render untrusted data as text, in the right context (HTML, attribute, URL, JS). Modern frameworks do this by default:
- React / Vue / Svelte: rendering
{value}escapes automatically. - Server templates: use auto-escaping templates; avoid "raw" helpers.
2. Avoid dangerous HTML rendering
If your framework offers a raw-HTML escape hatch, treat it as privileged: only for trusted authors, only on sanitized content, ideally isolated.
3. Sanitize when you must accept HTML
If users can submit HTML, run it through a vetted sanitizer (e.g. DOMPurify). Sanitization is genuinely hard — it must strip scriptable URLs (javascript:), event handlers (on*), and SVG/MathML edge cases — so don't roll your own.
4. Content Security Policy (CSP)
CSP limits the impact even if a bug exists. A reasonable baseline:
Avoid 'unsafe-inline' for scripts; use nonces or hashes instead, and add report-to to observe violations. See Security Headers.
Safe Patterns by Stack
Common Mistakes
Assigning untrusted input to innerHTML
Shipping a CSP that allows inline scripts
FAQ
Does input validation prevent XSS?
It helps, but it isn't sufficient. The reliable fix is context-aware output encoding when you render data. Validation reduces the attack surface; encoding closes the hole.
Is React immune to XSS?
No. React escapes values by default, which prevents most XSS — but dangerouslySetInnerHTML, javascript: URLs in href, and injecting into <script>/<style> can still introduce it.
What's the difference between stored and reflected XSS?
Reflected XSS executes from a single crafted request (the payload is in the URL or form). Stored XSS is persisted server-side and runs for every user who views it, which makes it far more dangerous.
Where does CSP fit in?
CSP is a backstop, not a primary fix. It limits what a successful injection can do (e.g. block inline scripts and exfiltration), buying safety margin while you fix the root cause.
Related Topics
- Security Headers — Configuring CSP and friends
- API Security — Validating and encoding API data
- Cross-Site Request Forgery (CSRF) — The other classic browser attack
- HTTPS & TLS — Protecting content in transit