Cross-site scripting, XSS, is a flaw that lets an attacker inject JavaScript which then runs in another user's browser as if your site sent it. Because that script executes with the victim's session and your site's privileges, it can steal cookies, capture keystrokes, perform actions as the user or rewrite the page. It is an injection flaw: untrusted input is treated as code.
XSS has been near the top of web risk lists for two decades because it is easy to introduce and its impact is severe. The good news is that the defences are well understood and, with modern frameworks, mostly automatic, provided you do not disable them.
Why XSS is dangerous
When script runs in the victim's browser under your origin, it inherits everything that origin can do. A successful XSS payload can read any cookie not marked HttpOnly, make authenticated requests as the user, capture what they type into a form, exfiltrate page contents, or silently redirect them. The browser's same-origin policy, normally a wall between sites, does not help here, because the script is running as your site.
HttpOnly blocks that specific step, which is why it is a standard defence.It is worth dwelling on why XSS is so persistent despite being understood for two decades. The web's fundamental design mixes code and content in the same document: HTML, CSS and JavaScript arrive together, and the browser executes what looks like script wherever it finds it. Any place your application takes data from a user and later renders it, a comment, a name, a search term, a URL parameter, is a place where content can masquerade as code if the boundary is not enforced. The vulnerability is not exotic; it is the default outcome of forgetting to encode one value in one template. That is also why it hides in the corners: the main flow is usually handled well, and the flaw lives in the error message, the admin view, or the rarely edited field.
The three types
Reflected XSS
The payload is in the request and echoed straight back in the response. A search page that prints You searched for: <term> without encoding lets an attacker craft a URL like:
https://example.com/search?q=<script>stealCookie()</script>
When a victim clicks that link, the script reflects into the page and runs. Reflected XSS needs the victim to follow an attacker-supplied link, so it is often paired with phishing.
Stored XSS
The payload is saved on the server, in a comment, a profile field, a product review, and served to everyone who views that page. No special link is required; every visitor is hit automatically. This is the most damaging type because one injection can compromise thousands of users, and it can spread worm-like if the affected field is widely viewed.
DOM-based XSS
The vulnerability is entirely in client-side JavaScript, which reads attacker-controllable input (from the URL fragment, say) and writes it into the page unsafely:
// Vulnerable: writes untrusted input straight into the DOM
element.innerHTML = location.hash.slice(1);
Here the server never sees the malicious payload; the flaw is in how the page's own script handles it. DOM XSS is easy to miss precisely because server-side testing does not reveal it.
| Type | Where the payload lives | Victim interaction |
|---|---|---|
| Reflected | In the request, echoed in the response | Must click a crafted link |
| Stored | Saved on the server | Just visits the page |
| DOM-based | Handled entirely in client JavaScript | Varies; often a crafted URL |
The layered defence
No single control stops XSS; the reliable approach is defence in depth, with each layer catching what the last missed.
1. Contextual output encoding (the primary fix)
Encode untrusted data for the exact context where it is inserted, HTML body, attribute, JavaScript, URL, so the browser treats it as text, not markup. The critical word is contextual: HTML-encoding a value that lands inside a <script> block does not protect it. Encode < as <, > as >, & as & and quotes appropriately for HTML contexts.
2. Use the framework's auto-escaping
React, Angular, Vue and modern template engines escape output by default. Most XSS in current codebases comes from deliberately bypassing that, React's dangerouslySetInnerHTML, Angular's bypassSecurityTrust, or building HTML by string concatenation. If you must render user-supplied HTML, sanitise it with a vetted library such as DOMPurify rather than trusting it.
3. Content Security Policy (the safety net)
A strong CSP means that even if a payload reaches the page, the browser refuses to execute it, because it lacks the required nonce or hash. CSP does not fix the injection, but it can turn a critical XSS into a blocked resource. Build one using our Content Security Policy guide; a missing or weak policy is flagged by the CSP check and weak-directives check.
4. HttpOnly cookies
Mark session cookies HttpOnly so injected script cannot read them, blocking the most common cookie-theft path. This is one of the cookie security flags, and our HttpOnly check reports cookies that lack it. Note it limits impact rather than preventing injection.
5. Consider Trusted Types for DOM XSS
DOM-based XSS is the hardest type to eliminate through code review, because it hides in client-side JavaScript that assigns untrusted data to a dangerous sink like innerHTML. Modern browsers offer Trusted Types, enabled through CSP, which forces those dangerous assignments to go through a vetted policy function rather than accepting raw strings. In effect it makes the unsafe pattern a runtime error unless the data has been explicitly sanitised, turning a whole class of DOM XSS from "possible if someone forgets" into "blocked by default." It requires refactoring, but for a large front-end codebase it is one of the few controls that scales.
The reason blocklists fail is instructive. A filter that strips <script> is trivially bypassed by an event handler like onerror on an image tag, by a javascript: URL, by mixed-case tags, by encoded characters the browser later decodes, or by breaking the payload across attributes the sanitiser reassembles. Every filter that tries to enumerate what is dangerous is a list the attacker only has to get around once, while the defender has to be right every time. Encoding flips that asymmetry: instead of guessing which inputs are hostile, you neutralise all input for the context it lands in, so a payload that slips past your imagination still renders as inert text.
Why external scanners rarely confirm XSS
Proving XSS means submitting a payload and observing it execute, which is active scanning. A passive scanner like Scan.now can flag contributing weaknesses, a missing CSP, input that reflects into the page, cookies without HttpOnly, but it will not fire an exploit. That boundary is explained in passive versus active scanning. XSS also sits inside the injection category of the OWASP Top 10, and old libraries are a frequent source, covered in outdated JavaScript libraries.
Verify your defences
Check the observable half of your XSS posture with the security headers checker (is a strong CSP present?) and the full website scanner (are cookies HttpOnly, are there weak headers?). Then close the gap that scanners cannot see, contextual encoding in your own code, because that is where XSS is actually prevented. Everything here connects back to the website security hub.
If you take one organising idea from this guide, make it the layering, because it is what turns XSS from an ever-present threat into a manageable one. Output encoding and framework escaping stop the injection from happening in the first place; that is the wall. A strong CSP stops an injection that slips through from executing; that is the moat. HttpOnly cookies stop an executing script from stealing the session; that is the vault. Trusted Types close the DOM-based back door that code review misses. No single layer is sufficient, and, crucially, none is meant to be: each exists precisely because the others will occasionally fail. A team that ships all four has made XSS a low-probability, low-impact event even though the underlying web platform still mixes code and content in the same document. That is the realistic goal, not to make XSS impossible, which the platform will not allow, but to make every individual failure survivable, so the one forgotten encoding does not become the one catastrophic breach.