Cross-site scripting (XSS): how it works and how to stop it
What cross-site scripting is, the three main types, why it still appears in modern applications, and the layered defences that reliably prevent it.

Cross-site scripting (XSS) is one of the oldest web vulnerabilities and still one of the most common. It sits under Injection in the OWASP Top 10. This article explains how it works, why modern frameworks don’t fully prevent it, and which defences actually do.
What XSS is
XSS happens when an application includes untrusted data in a web page without making sure the browser treats it as text rather than code. If an attacker can get their own JavaScript to run in another user’s browser, on your domain, that script can do anything the user can:
- read data shown on the page
- perform actions as the user, such as changing their email address or approving a payment
- capture what the user types, including passwords on a fake login form
- steal session tokens that aren’t protected
The three main types
| Type | Where the malicious input comes from | Typical example |
|---|---|---|
| Reflected | The current request, usually a URL parameter | A search page that echoes the search term back without encoding |
| Stored | Data saved by the application | A comment, profile name or support ticket shown to other users, often including administrators |
| DOM-based | Client-side JavaScript that writes data into the page | Code that takes part of the URL and inserts it with innerHTML |
Stored XSS is usually the most serious, because it can hit every user who views the affected page, including staff with high privileges.
Why it still happens in modern applications
Frameworks such as React, Angular and Vue encode output by default, which prevents most XSS. It still appears through:
- Escape hatches such as
dangerouslySetInnerHTML,v-htmlor[innerHTML], often used to render “rich text” - Rich text editors and Markdown rendered to HTML without a robust sanitiser
- URLs placed in links or redirects without checking the scheme, allowing
javascript:links - Third-party widgets and legacy pages outside the main framework
- Server-side templates where encoding is disabled or applied in the wrong context
How to prevent it: layered defences
1. Encode output for its context
Data placed in HTML, attributes, JavaScript, CSS and URLs each needs different encoding. Let your framework’s templating do this and avoid building HTML by string concatenation.
2. Avoid dangerous sinks
Prefer textContent over innerHTML. When you genuinely must render user-supplied HTML, pass it through a well-maintained sanitiser (such as DOMPurify) with a strict allow-list, on the server as well as the client.
3. Validate URLs
Only allow http: and https: (and mailto: where needed) in user-supplied links and redirects.
4. Content Security Policy (CSP)
A strict CSP that blocks inline scripts and limits script sources turns many XSS bugs from “full account takeover” into “harmless”. It is a safety net, not a replacement for encoding.
5. Protect session cookies
Mark session cookies HttpOnly, Secure and SameSite, so a script can’t read them directly.
How we test for it
In a web application test we map every place user input is stored or reflected, including in admin-only views, and test each output context separately. We also review client-side JavaScript for DOM-based sinks, and check that CSP and cookie flags would limit the impact if something slipped through.
Quick checklist
- Framework auto-escaping on everywhere; escape hatches reviewed
- User-supplied HTML sanitised with an allow-list
- Link and redirect URLs restricted to safe schemes
- Strict Content Security Policy in place
- Session cookies set to
HttpOnly,Secure,SameSite
XSS is rarely hard to fix once found. The difficulty is finding every instance, which is exactly what a manual web application test is for.