CSRF vs XSS Mitigation

TL;DR / CSRF exploits the browser's automatic cookie attachment to forge requests as the victim, while XSS injects malicious scripts that execute within the victim's origin -- they require fundamentally different defenses.

How It Works

 ┌────────────────┐            ┌────────────────┐
 │      CSRF      │            │      XSS       │
 └────────────────┘            └────────────────┘
          │ Forged request              │ Injected script
          │ from victim's               │ runs in victim's
          │ browser                     │ origin
          │                             │
          │                             │
          │                             │
          ↓                             ↓
 ┌────────────────┐            ┌────────────────┐
 │     Token      │            │      CSP       │
 │    SameSite    │            │    Sanitize    │
 │  Origin check  │            │ Escape output  │
 └────────────────┘            └────────────────┘

Edit diagram

These two attacks are frequently confused, but they operate on entirely different axes. Understanding the distinction is critical because applying CSRF defenses against XSS (or vice versa) provides zero protection.

CSRF: Exploiting Authentication Context

In a CSRF attack, the attacker never runs code in your application's origin. Instead, they create a page on evil.com containing a form or image tag that triggers a request to bank.com/transfer. When the victim visits evil.com, their browser automatically attaches any cookies for bank.com to that request. The server sees a valid session cookie and processes the forged request as if the user initiated it.

The attacker cannot read the response (the Same-Origin Policy blocks that). They do not need to -- the damage is in the state change: transferring money, changing a password, modifying settings. CSRF is a write-only attack.

CSRF Defenses:

Synchronizer tokens are the classic defense. The server generates a random token per session (or per request) and embeds it in forms. The token is not a cookie, so the attacker cannot include it in forged requests. The server rejects any request missing a valid token.

The SameSite cookie attribute provides automatic protection by preventing cookies from being sent on cross-site requests. Lax mode blocks cross-site POST requests while allowing same-site navigation. Checking the Origin or Referer header on the server provides another layer -- forged requests from evil.com carry that origin, not yours.

Double-submit cookies work by setting a random value in both a cookie and a request parameter. The server verifies they match. Since the attacker cannot read the cookie value from another origin, they cannot include the matching parameter.

XSS: Executing Code in the Victim's Origin

XSS is fundamentally different. The attacker injects JavaScript that runs within your application's origin. The script has full access to the DOM, cookies (unless HttpOnly), localStorage, and can make authenticated requests indistinguishable from legitimate user actions. XSS bypasses CSRF protections entirely because the malicious script runs in the same origin and can read CSRF tokens from the page.

Stored XSS persists the payload in the database (a comment, profile field, etc.) and executes for every user who views it. Reflected XSS includes the payload in a URL parameter that gets rendered into the page without sanitization. DOM-based XSS manipulates the client-side DOM through user-controlled input, never touching the server.

XSS Defenses:

Output encoding is the primary defense. Every dynamic value inserted into HTML, JavaScript, CSS, or URLs must be escaped for that specific context. HTML-encoding < as &lt; prevents tag injection. JavaScript string escaping prevents breakout of string literals. Using framework-provided templating (React's JSX, Vue's templates) handles this automatically for HTML contexts.

Content Security Policy (CSP) restricts which scripts can execute. A strict CSP with nonces or hashes blocks inline scripts even if an attacker manages to inject them. script-src 'nonce-random' requires every script tag to carry a server-generated nonce.

Input sanitization libraries like DOMPurify strip dangerous HTML while preserving safe markup. This is essential when you must accept rich text from users. Sanitize on output, not just input, because storage context and rendering context may differ.

Why They Require Different Defenses

CSRF tokens protect against forged requests but are useless against XSS -- the injected script simply reads the token. CSP blocks injected scripts but does nothing against CSRF -- the attacker's forged request comes from their own page, not an injected script. SameSite cookies stop cross-site request forgery but cannot prevent scripts executing within the same origin.

A complete defense requires both sets of protections applied simultaneously.

Gotchas

  • XSS defeats CSRF protections. If an attacker achieves XSS, they can read CSRF tokens, make authenticated requests, and bypass SameSite cookies because they operate within the same origin.
  • CSRF does not require JavaScript. A simple <img src="bank.com/transfer?to=attacker"> or auto-submitting form is enough. Disabling JavaScript does not prevent CSRF.
  • Output encoding must be context-aware. HTML-encoding a value placed inside a <script> tag or onclick attribute does not prevent XSS. Each context (HTML body, attribute, JavaScript, URL) has different escaping rules.
  • HttpOnly cookies are not an XSS defense -- they prevent cookie theft but XSS scripts can still make authenticated requests using the browser's cookie jar.
  • CSP unsafe-inline negates most XSS protection. Many legacy apps add this to "make CSP work" and gain almost no security benefit from the policy.