Stored Cross-Site Scripting
Summary
Stored XSS reports follow predictable shapes:
- profile fields - name, bio, company, signature reflected on viewing pages
- message bodies - chat, comment, review, support ticket
- file-upload-to-XSS - uploading SVG, HTML, PDF, or via Content-Type confusion (text/html with .png extension)
- blind XSS - payload fires on admin/back-office page (xsshunter.com pattern)
- markdown / WYSIWYG / link href accepting javascript: scheme
- integration / app-config fields rendered raw
- email subject/body XSS rendered in webmail. The blind-XSS subset is the most damaging because admin sessions yield admin cookies. CVE markers exist for products like Rocket.Chat, Nextcloud, ImpressCMS.
Top Affected Components / Targets
Profile / avatar / bio / signature / company name fieldsComment / review / message / chat / ticket bodiesFile upload endpoints (SVG, HTML, PDF, image)Markdown / WYSIWYG / rich-text editorsLink/anchor href fieldsAdmin moderation queue (where blind XSS fires)Email subject/body rendered in webmailOAuth app metadata (name, description, redirect URL)Report / dashboard / widget label fieldsWebhook configuration name / description
Common Attack Vectors
Inject XSS payload into profile fields and trigger by viewing the profileUpload SVG containing <script> as avatar / inline imageUpload HTML file with renamed extension and Content-Type header trickInject blind XSS payload into contact / support / signup forms (xsshunter.com)Inject markdown-tag-confusion payload (Rocket.Chat CVE-2021-22886 style)Add link with javascript:alert(1) URL in markdown editorsStore payload in field shown in admin moderation queueInject into review/comment fields rendered on product pageInject into mention/tag/hashtag fields rendered as anchor textInject into custom report/widget label fields
Common Payloads
<svg/onload=alert(1)><img src=x onerror=alert(document.domain)><script src=https://attacker.xss.ht></script><a href="javascript:alert(1)">click</a>[click](javascript:alert(1)) (markdown)[click](java%0Ascript:alert(1))<svg><script>alert(1)</script></svg> (SVG file upload)<details ontoggle=alert(1) open><form><button formaction=javascript:alert(1)>x</button></form><input autofocus onfocus=alert(1)>
Detection Strategy
For every form field discovered in recon, write a unique alphanumeric token and immediately fetch the page where the value is shown; if reflected without encoding, escalate.
On every upload endpoint, try the variants - SVG-with-script, HTML-with-image-extension, .svg with multiple Content-Types, polyglot.
Emit blind-XSS payloads tied to a per-target unique OOB token and watch for callback on a per-target callback infrastructure (Burp Collaborator-style).
In markdown-accepting fields, try [x](javascript:alert(1)) and link-newline tricks.
Track every place a stored field appears (multiple pages); only flag XSS when the payload executes (or would, sans CSP).
Confidence: in-band execution = critical; reflected-but-CSP-blocked = high; blind-XSS callback fires = high.
Tip: Testing tools that run these checks in parallel across every discovered endpoint can cut the time required substantially compared to fully manual testing, as long as they confirm findings with more than one signal to keep the false-positive rate down.
False-Positive Notes
- Some apps store raw HTML but escape on render in the canonical view, while leaking it in admin or PDF export - coverage requires probing multiple render locations.
- SVG XSS only fires when served as image/svg+xml from the same origin (or when inlined); flag conditional on those.
- Blind XSS callbacks can occur from headless browsers (sentry, WAF crawlers) - require browser headers / cookies in the callback to confirm a real admin viewer.
How to Test
Manual Testing Methodology
Here is a systematic approach to identifying Stored Cross-Site Scripting vulnerabilities in a target application.
Before testing, map all input vectors that could be affected. Identify parameters, headers, cookies, and request bodies that interact with the vulnerable component. A proxy such as Burp Suite or OWASP ZAP, paired with normal browsing of the target, is usually enough to build this list.
Send a legitimate request and record the normal response: status code, content length, response time, and any identifying tokens. This baseline matters because it's what you'll compare later responses against once payloads are involved.
Inject test payloads into each identified input vector one at a time. Start with benign detection payloads before escalating to anything that could actually trigger the vulnerability. For Stored Cross-Site Scripting specifically, inject a unique, easily-searchable marker string into every parameter first to map which ones reflect at all, then follow up on the reflecting ones with context-appropriate breakout payloads — "><svg/onload= for an HTML/attribute context, ';alert(1);// for a JavaScript string context, javascript: for a URL context.
Compare the response against your baseline, looking specifically for whether the marker or payload comes back unencoded and in an executable position — a reflected < that renders as < in the page source is evidence of correct encoding, not a finding, no matter how the marker itself looks in a diff.
Once a potential vulnerability is detected, confirm it with at least a few independent test cases to rule out coincidence. Document the exact request and response as proof. For Stored Cross-Site Scripting, a confirmed finding typically means showing that attacker-controlled input changes the application's behavior in a way that matters for security, not just that a payload was reflected somewhere harmless.
Real-World Impact
Real-World Impact
Cross-site scripting lets attackers execute arbitrary JavaScript in a victim's browser, effectively hijacking their authenticated session. This leads to session token theft, credential harvesting through fake login forms, cryptocurrency mining in the browser, defacement, and in social platforms, worm-like propagation from one infected profile to the next.
Stored XSS is particularly severe because it can affect every user who views the poisoned content, meaning a single injection point can compromise thousands of sessions at once. It's also frequently chained with CSRF to perform administrative actions or exfiltrate internal data once an attacker has script execution in an authenticated context.
XSS remains one of the most commonly reported vulnerability classes across web applications of every size, which is part of why it still shows up so often in disclosed reports despite being well understood.
Prevention & Remediation
Prevention and Secure Coding
Preventing Stored Cross-Site Scripting takes a defense-in-depth approach — no single control below is sufficient alone, but together they close off both the primary path and the most common bypasses.
Context-aware output encoding. Encode output for the exact context it lands in — HTML body, HTML attribute, JavaScript string, or URL — at render time, not once globally; the same value needs different encoding depending on where it's placed.
Auto-escaping frameworks. Modern templating and component frameworks (React, Vue, Angular) escape output by default — the actual risk concentrates in the explicit escape hatches (dangerouslySetInnerHTML, v-html, [innerHTML]) that bypass that protection, so audit those call sites specifically.
Content-Security-Policy. A script-src CSP built on nonces or hashes (not unsafe-inline) stops injected inline scripts from executing even if a payload gets through the encoding layer.
Allowlist sanitization for rich text. Where users are meant to submit formatted content, sanitize it server-side with an allowlist HTML sanitizer, not a denylist regex — denylists are reliably bypassable.
Cookie flags as a backstop. HttpOnly on session cookies limits the blast radius of a successful injection by keeping the token out of reach of document.cookie even if script execution succeeds.
Source reports
A sample of the disclosed HackerOne reports this playbook was synthesized from.