OWASP Top 10 Deep Dive · 3 of 8

XSS — Someone Else's JavaScript on Your Page

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.

Stored XSSReflected XSSDOM XSSCSPEncoding
← Back to Security
Quick Facts

What XSS Is

Basic Concepts

  • Same-origin model: JavaScript in your page can read your cookies, your localStorage, and call your authenticated APIs. Anyone who runs JS in your origin is your origin.
  • The pattern: attacker input ends up rendered as HTML/JS instead of escaped as text. <script>steal()</script> in a comment field that gets shown back unescaped.
  • Three flavors: stored (saved on the server, served to all viewers), reflected (in a URL or form, served back in the response), DOM-based (no server involvement, JS reads attacker input from location.hash and writes it into the DOM).
  • Severity: high. Account takeover, session theft, MFA bypass via UI manipulation, malicious actions performed as the victim.
The Family

The Three Flavors

Stored XSS

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.

Reflected XSS

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.

DOM-Based XSS

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).

Defenses

How to Stop XSS

1. Context-Aware Output Encoding

The single most effective defense. The encoding required depends on where the value lands:

  • HTML body: escape < > & " as entities.
  • HTML attribute: additionally escape quotes; use quoted attributes always.
  • JavaScript context: serialize as JSON in a <script type="application/json"> tag and parse from JS — don't interpolate into a JS literal.
  • URL: percent-encode; validate scheme (no javascript:).
  • CSS: avoid; if necessary, escape with care.

Modern frameworks (React, Angular, Vue, Svelte, Razor, Thymeleaf, Django templates) escape by default. Use them. Audit any place you opt out (dangerouslySetInnerHTML, v-html, {!...}).

2. Content Security Policy (CSP)

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.

3. Sanitize When You Must Render HTML

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.

4. Validate URLs Before Rendering as Links

<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.

5. Cookie Hygiene Limits the Damage

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.

6. Trusted Types (Modern Browsers)

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.

Frameworks

Where Frameworks Help — and Where They Don't

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:

  • Explicit escapes — dangerouslySetInnerHTML, v-html, {!...}, raw template helpers. Search the codebase regularly.
  • Attribute and URL contexts where the framework's auto-escape isn't enough — passing user input into href or style needs URL/CSS-aware sanitizing.
  • DOM-based sinks driven by client-side code reading location, postMessage, third-party scripts.
  • Server-rendered HTML strings concatenated outside the framework — emails, PDFs, exports.
Continue

Other OWASP Top 10