The Execution Model: How Browsers Execute Injected Scripts
Cross-Site Scripting (XSS) occurs when malicious JavaScript is injected into trusted web applications and executed by victims' browsers.
XSS Vulnerability Classes & Execution Contexts Matrix
| XSS Classification | Injection Point & Persistence | Primary JavaScript Sinks | Defensive Countermeasure |
|---|---|---|---|
| Stored XSS (Persistent) | Saved in database (comments, profiles, forums) | innerHTML, outerHTML, unescaped server templates | Strict contextual output encoding, DOMPurify sanitization |
| Reflected XSS (Non-Persistent) | Embedded in search query or URL parameter | Reflected in immediate HTTP HTML response | Framework auto-escaping, Content Security Policy (CSP) |
| DOM-based XSS | Client-side JavaScript DOM manipulation | location.search, eval(), document.write() | W3C Trusted Types API, textContent instead of innerHTML |
| Mutation XSS (mXSS) | Browser HTML parser mutating seemingly safe markup | SVG foreignObjects, MathML namespace switches | Audited HTML sanitizers (DOMPurify with mXSS protection) |
The browser's HTML parser interprets text streams sequentially, building the Document Object Model (DOM). When the parser encounters characters like <, >, ", or ' in an unescaped context, it transitions into an execution state, treating incoming strings as HTML elements or JavaScript expressions.
Once an attacker successfully executes JavaScript in the victim's browser, they gain the full privileges of the authenticated user. Typical exploit payloads accomplish:
- Session Hijacking: Stealing session tokens stored in
localStorage,sessionStorage, or accessible cookies (document.cookie). - Credential Harvesting: Injecting fake login forms directly into the active webpage to capture passwords or MFA codes.
- Silent API Manipulation: Making background
fetch()requests on behalf of the user to change email addresses, transfer funds, or generate API keys. - Keylogging: Attaching event listeners (
document.addEventListener('keydown')) to transmit keystrokes to an external server.
The Three Types of XSS Compared
XSS vulnerabilities are classified into three primary types based on where the payload is stored and how it reaches the browser:
| Type | Persistence | Delivery Mechanism | Common Locations | Primary Defense |
|---|---|---|---|---|
| Stored (Persistent) | Permanent (Database, file) | Loaded automatically from database on page visit | Comments, user profiles, forum posts, chat logs | Context-aware output encoding & sanitization |
| Reflected (Non-Persistent) | None | Embedded in link/URL and reflected immediately in HTTP response | Search results, error messages, login redirect params | Output encoding of request parameters |
| DOM-Based | Client-side memory/DOM | Executed purely client-side via JavaScript without server reflection | location.hash, document.referrer, window.name | Safe DOM APIs & Trusted Types |
DOM XSS: Dangerous Sinks vs. Safe Primitives
DOM-based XSS occurs entirely in the client-side JavaScript execution environment. An untrusted input (Source) flows into an execution function (Sink) without adequate sanitization.
Dangerous DOM Sources (Untrusted Input)
location.search(URL query parameters)location.hash(URL fragments)document.referrerwindow.namepostMessage()events without origin verification
Dangerous Execution Sinks
element.innerHTML = untrustedData;element.outerHTML = untrustedData;document.write(untrustedData);eval(untrustedData);setTimeout(untrustedString, 100);location.href = untrustedUrl;(allowsjavascript:...pseudo-protocols!)
Safe DOM Alternatives
- Use
element.textContentinstead ofelement.innerHTML - Use
document.createElement()andelement.setAttribute()instead of concatenating HTML strings
// VULNERABLE DOM XSS Pattern
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
// If name contains <img src=x onerror=alert(1)>, script executes immediately!
document.getElementById('greeting')!.innerHTML = `Hello, ${name}`;
// SAFE DOM Pattern
const safeElement = document.getElementById('greeting')!;
safeElement.textContent = `Hello, ${name}`; // Browser treats string strictly as textModern Frontend Vulnerabilities: React, Next.js & Vue
Modern frameworks like React automatically encode values rendered inside JSX curly braces ({userContent}), converting < to < and > to >. However, developers frequently bypass these protections:
- `dangerouslySetInnerHTML` in React:
// VULNERABLE: Bypasses React's automatic encoding
<div dangerouslySetInnerHTML={{ __html: post.content }} />- URL Attribute Injection (`javascript:` URIs):
React does NOT validate URL protocols in anchor tags. If user-supplied input flows into href, attackers can submit javascript:alert(document.domain):
// VULNERABLE if websiteUrl starts with 'javascript:'
<a href={user.websiteUrl}>Visit Website</a>- Vue.js `v-html` Directive: Similar to React's dangerous HTML,
v-htmlexecutes raw scripts if user content is passed un-sanitized.
// Safe URL validation helper for React anchor tags
export function sanitizeUrl(url: string): string {
const trimmed = url.trim().toLowerCase();
// Disallow dangerous pseudo-protocols
if (
trimmed.startsWith('javascript:') ||
trimmed.startsWith('data:') ||
trimmed.startsWith('vbscript:')
) {
return '#'; // Fallback to safe destination
}
return url;
}Context-Aware Output Encoding Rules
Output encoding must match the exact context where data is inserted. Standard HTML entity encoding (<) is insufficient if data is placed inside an attribute or script tag:
- HTML Body Context: Convert
&to&,<to<,>to>,"to",'to'. - HTML Attribute Context: Inside
<input value="...">, attributes must be attribute-encoded to prevent breaking out of quotes. - JavaScript Variable Context: Inside
<script>var x = '...';</script>, use JSON serialization with Unicode escaping (\u003Cfor<) to prevent script tag termination (</script>). - URL Parameter Context: Use standard percent-encoding via
encodeURIComponent().
Industrial-Strength Sanitization with DOMPurify
When applications must accept rich text (e.g., Markdown or WYSIWYG HTML), sanitization must strip executable elements while preserving benign formatting tags (<b>, <i>, <p>). Use a battle-tested library like DOMPurify:
import DOMPurify from 'isomorphic-dompurify';
export function renderUserHtml(untrustedHtml: string) {
// Configure strict allowlist of allowed tags and attributes
const cleanHtml = DOMPurify.sanitize(untrustedHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'li', 'code'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}Content Security Policy (CSP Level 3) as Defense-in-Depth
Content Security Policy (CSP) is an HTTP response header that restricts the resources (scripts, images, stylesheets, frames) the browser is allowed to load and execute. Even if an attacker injects an <script> tag into the DOM, a strict CSP prevents its execution.
Modern Nonce-Based CSP Policy
Modern CSP architectures use cryptographic random nonces generated per HTTP request:
# Modern Strict CSP Header (Nginx or Next.js middleware)
Content-Security-Policy: \
default-src 'self'; \
script-src 'self' 'nonce-rAnd0m12345' 'strict-dynamic'; \
object-src 'none'; \
base-uri 'none'; \
frame-ancestors 'none';Modern Defenses: W3C Trusted Types API & Next.js Nuances
The W3C Trusted Types API
Trusted Types locks down dangerous DOM sinks (like element.innerHTML and script.src) at the browser engine level. When enforced via CSP (Content-Security-Policy: require-trusted-types-for 'script'), the browser throws a runtime TypeError if plain strings are assigned to DOM sinks:
// Enforcing Trusted Types in Modern JavaScript
if (window.trustedTypes && trustedTypes.createPolicy) {
const sanitizePolicy = trustedTypes.createPolicy('dompurify-policy', {
createHTML: (input: string) => DOMPurify.sanitize(input),
});
// Browser accepts only TrustedHTML instances; rejects plain strings!
const safeHTML = sanitizePolicy.createHTML(userComment);
document.getElementById('content')!.innerHTML = safeHTML as unknown as string;
}Next.js & React Auto-Escaping Gotchas
While React escapes strings rendered inside JSX expressions (<div>{userText}</div>), three dangerous escape hatches remain:
- `dangerouslySetInnerHTML`: Bypasses React escaping entirely.
- URL Attribute Protocol Injection:
<a href={userLink}>executes JavaScript if the user submitsjavascript:alert(1). - SVG File Uploads: SVGs containing
<script>tags execute arbitrary JavaScript when viewed directly in the browser.
Common Mistakes to Avoid
❌Relying solely on blacklisting specific script tags like `<script>`
Why it happens: Developers write regex to strip `<script>` and `</script>` tags from input.
Why it matters: JavaScript can execute via hundreds of HTML tags and event handlers: `<img src=x onerror=alert(1)>`, `<svg onload=alert(1)>`, `<body onpageshow=...>`.
Correct approach: Use context-aware output encoding or DOMPurify rather than custom regex tag-stripping.
❌Storing sensitive authentication tokens in localStorage
Why it happens: Developers find `localStorage.setItem('token', jwt)` convenient for SPA authentication.
Why it matters: `localStorage` is accessible to ANY JavaScript executing on the origin. A minor XSS vulnerability immediately leads to full account takeover.
Correct approach: Store session tokens in `HttpOnly; Secure; SameSite=Strict` cookies that cannot be read by JavaScript.
❌Passing untrusted user input directly to `href` in anchor tags
Why it happens: Developers assume React automatically sanitizes URLs.
Why it matters: Attackers supply `javascript:alert(document.domain)`. When the user clicks the link, the script executes in the user's session.
Correct approach: Validate user URLs against an allowlist of permitted protocols (`http:`, `https:`, `mailto:`).
❌Using `unsafe-inline` in Content Security Policy without nonces
Why it happens: Teams enable `script-src 'unsafe-inline'` to make legacy third-party scripts work.
Why it matters: `unsafe-inline` completely disables CSP protection against inline XSS script injections.
Correct approach: Migrate to cryptographic request nonces (`'nonce-...'`) and `'strict-dynamic'`.
❌Sanitizing input upon insertion into the database instead of encoding at output
Why it happens: Developers attempt to clean input data once at API entry points.
Why it matters: Input may be used in multiple contexts (HTML, PDF generation, email, JSON API, mobile app). Sanitizing for one context breaks or under-protects others.
Correct approach: Store raw data in the database and apply context-aware encoding or sanitization at the exact point of rendering.
Troubleshooting Guide
Problem: DOMPurify is stripping legitimate links or styles from rich-text content
Possible Causes:
- Default DOMPurify configuration strips custom attributes or inline CSS.
How to verify: Inspect the output of `DOMPurify.sanitize(input)` in console.
How to fix: Configure explicit `ALLOWED_TAGS` and `ALLOWED_ATTR` options in DOMPurify settings.
Problem: Browser console logs 'Refused to execute inline script because it violates Content Security Policy'
Possible Causes:
- An inline `<script>` tag was loaded without the matching cryptographic nonce.
- An inline event handler (`onclick=...`) was used in HTML.
How to verify: Inspect the CSP header in network tab and verify if the nonce matches the script's `nonce` attribute.
How to fix: Attach event listeners via `addEventListener` in external scripts and supply matching `nonce` attributes.
Problem: React hydration error occurs when using DOMPurify on server and client
Possible Causes:
- Different DOM parsing environments between Node.js (JSDOM) and browser.
How to verify: Check Next.js console for 'Text content did not match server-rendered HTML'.
How to fix: Use `isomorphic-dompurify` or sanitize rich text inside a `useEffect` hook on client-side mount.
Actionable Checklist
Wescequre Platform · Scanner Engine
Automated Vulnerability Scanner
Launch targeted or full-domain security audits to detect OWASP Top 10 vulnerabilities automatically.
Includes: OWASP Top 10 automated test suites · Real-time severity scoring (CVSS v3.1) · Instant remediation code snippets
Frequently Asked Questions
The Same-Origin Policy prevents a script loaded on `evil.com` from reading data on `example.com`. However, XSS injects malicious code directly into `example.com`. Because the script runs *within* `example.com`, SOP treats it as trusted.
No. HttpOnly cookies prevent an attacker from *stealing* the cookie via `document.cookie`. However, an attacker executing XSS can still make authenticated background requests (`fetch('/api/transfer')`), log keystrokes, and deface the page.
Trusted Types is a W3C specification supported in Chromium browsers that enforces type-safety on dangerous DOM sinks. It blocks direct string assignment to `innerHTML`, requiring values to pass through a registered sanitization policy.
No. Input validation ensures data adheres to expected business formats (e.g., verifying an email structure). However, legitimate names like `O'Connor` or mathematical equations contain characters (`'`, `<`, `>`) that can trigger XSS if not encoded at output.
A CSP nonce is a cryptographically strong random token generated by the server for each HTTP response. The server places the nonce in the CSP header (`script-src 'nonce-XYZ'`) and in trusted inline `<script nonce="XYZ">` tags. The browser only executes scripts with matching nonces.
Authoritative Sources & References
- OWASP Cross Site Scripting (XSS) Prevention Cheat SheetOWASP Foundation (official)View Source
- W3C Content Security Policy Level 3 SpecificationW3C (official)View Source
- CWE-79: Improper Neutralization of Input During Web Page GenerationMITRE (official)View Source
- W3C: Trusted Types Specification for DOM XSS PreventionW3C (official)View Source
Related Guides
Continue exploring related technical architecture and defensive guides
security headers · CSP
Complete Guide to HTTP Security Headers (CSP, HSTS & More)
Master the essential HTTP security headers: CSP Level 3, HSTS preloading, clickjacking defense, MIME sniffing prevention, and production server configurations.
input validation · sanitization
Input Validation & Sanitization: Defensive Programming Guide
Developer blueprint for input validation: schema-based allowlists with Zod, ReDoS catastrophic backtracking defense, and safe multipart file upload pipelines.
sessions · cookies
Secure Session Management: Cookies, Tokens & Best Practices
Technical guide to session security: cryptographically secure cookie prefixes (`__Host-`), session fixation defense, idle timeouts, and Redis session architectures.
