The application places attacker-controlled text into a page without encoding it for its context, so the browser runs it as script. Stored XSS persists in the database and fires for every visitor, reflected XSS arrives in a crafted link, and DOM-based XSS happens entirely in client-side JavaScript.
At a glance
- Why it matters
- Script running in a victim's session can read what the victim sees, act as the victim (change email, approve a transfer), capture credentials through a fake login prompt and spread to other users. Against staff or admin users one stored payload can lead to full application takeover.
- How to fix it
- Encode output for the exact context (HTML body, attribute, URL, JavaScript) and prefer frameworks that do it by default. Sanitise any HTML you must accept with a maintained allow-list library. Add a strict Content-Security-Policy without 'unsafe-inline', and mark session cookies HttpOnly so script cannot read them.
Description
Cross-site scripting happens when an application includes untrusted input in a page it renders without properly encoding it for the context, so the input is interpreted as script rather than as text or data. Stored XSS persists the payload server-side (a comment field, a profile name) so it fires for every visitor who views it; reflected XSS carries the payload in the request itself, typically a URL parameter, and fires only for the victim who follows a crafted link; DOM-based XSS never touches the server at all, the vulnerable sink and source are both in client-side JavaScript.
Impact
A successful XSS payload runs with the same origin and privileges as the page itself, which typically means it can read the page's storage and any session cookie not marked HttpOnly, perform actions as the victim (anything the victim's own click could do), rewrite the page to phish credentials, or pivot into distributing further payloads to other visitors. Stored XSS on an admin-facing page is a particularly severe variant: it can compromise a staff account with far higher privileges than the attacker who planted it ever had.
How we find it
- We inject a distinct marker payload into every input we can reach (form fields, URL parameters, headers, file names, uploaded document metadata) and then check every place that value is later rendered, including pages other than the one it was submitted from.
- For reflected and DOM-based XSS we trace the data flow with the browser's developer tools, watching for a sink (
innerHTML,document.write, a templating call marked as raw or unescaped) that receives attacker-influenced input. - We test context-specific bypasses, since encoding that is correct for an HTML body is often wrong for an attribute, a
<script>block, a URL, or a CSS value; a payload built for the wrong context is exactly how encoding gaps get missed. - Where a Content-Security-Policy is present, we check whether it would actually block the payload we found (an overly permissive
script-srcwithunsafe-inlinedefeats most of the protection CSP is meant to provide).
Fix
Encode output for the context it is rendered in (HTML body, HTML attribute, JavaScript string, URL, CSS) at the point of output, using your framework's auto-escaping rather than manual string building. Pair this with a Content-Security-Policy that restricts script sources, since it stops a missed encoding call from becoming an executable payload.
// Vulnerable: user-controlled value inserted as raw HTML
element.innerHTML = "Welcome, " + userSuppliedName;
// Fixed: rendered as text, never parsed as markup
element.textContent = "Welcome, " + userSuppliedName;
// If HTML must be rendered, encode explicitly for the HTML body context
function escapeHtml(s) {
return s.replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
}[c]));
}
element.innerHTML = "Welcome, " + escapeHtml(userSuppliedName);
References
- OWASP Top 10: A03:2021, Injection
- CWE-79: Improper Neutralization of Input During Web Page Generation
- OWASP Cross Site Scripting Prevention Cheat Sheet
Practise it in the lab XSS Playground: No Filter.