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.

Abstract code on a screen, representing cross-site scripting

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-html or [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.