Scanners miss stored XSS because the attack needs two steps in two places: a payload saved by one request, then rendered by a different page, often one the scanner never visits or cannot see. You close the gap with authenticated crawling, unique markers you search for later, and manual checks of admin views.
What stored XSS is
Stored cross-site scripting happens when an application accepts input, saves it, and later shows it to users without making it safe for the browser. The OWASP Web Security Testing Guide notes that this usually involves at least two requests, which is why it is also called second-order XSS. It also points out that the victim does not need to click a crafted link. Opening the page that holds the data is enough.
That second property is what makes it serious. A comment, a profile name or a support ticket can run script in the browser of whoever reads it next, including staff with high privileges.
Why the two-step shape trips up scanners
A scanner that tests reflected XSS gets its answer in the same response: send a probe, look for it in the reply. Stored XSS breaks that loop in several ways.
- The probe is accepted on a form, but the page that renders it is a different URL.
- The rendering page sits behind a role the scanner does not have, such as a moderation queue or an admin dashboard.
- The data appears only after a delay, an approval step, or a background job.
- The data leaves the web app entirely. The testing guide tells testers to look at out-of-band channels such as email or integrations with external applications that receive and store user input.
- The scanner has no way to tell which of a thousand pages shows the value it saved ten minutes ago.
None of this means scanners are useless here. It means coverage depends on how you set up the scan.
How to close the gap
- Scan while signed in. Many stored inputs sit behind login: profiles, carts, settings, tickets. Use at least two roles so content written by a low-privilege user is also viewed as a higher one.
- Use unique markers. Submit a harmless, unique string per field (for example a short random token tied to the field name). Then search every page, export, email and log view for that token and see how it is encoded on the way out.
- List every place input is stored. The testing guide names profile pages, shopping carts, file managers, application settings, forums, blogs and logs. Check URL paths, cookies and headers too, not only form fields.
- Look at the reading side. For each stored value, find every screen that displays it. The riskiest are the ones staff use.
- Check the encoding, not just the alert. If a marker comes back with angle brackets and quotes turned into entities, that context is handled. If it comes back raw, you have a finding even before you prove script execution.
- Fix at output. Context-sensitive output encoding is OWASP's primary defense for server-side XSS. Validate input too, but treat that as a second layer.
| Gap | What helps |
|---|---|
| Payload and render on different pages | Unique markers, search across the site afterward |
| Render page needs a higher role | Authenticated scans with more than one role |
| Delayed or moderated display | Re-crawl after the queue runs, test by hand |
| Output goes to email or exports | Manual review of those channels |
Try it yourself
Click through it at your own pace, or open it full screen. Watch how a crawl finds forms and what it reports for each input it tests.
Quick check
Can a scanner find stored XSS at all?
Yes, when it can reach both the input and the page that displays it, and when it can match what it stored to where it shows up. It struggles with delayed, role-gated or out-of-band output, so test those by hand.
Is stored XSS worse than reflected XSS?
OWASP describes stored XSS as the most dangerous type because victims need no crafted link and privileged users may be exposed. Severity still depends on who views the data and what the page can do.
If you want a quick look at your dependencies as a starting point, Safeguard offers a free scan at gold.safeguard.sh/scan, and its policies and gates can block risky changes at pull request time.
Sources
Last reviewed: 19 October 2026.