CSRF vs XSS: Understanding the Difference and How to Mitigate Both
Security vulnerabilities often sound similar until you understand exactly what each one abuses. CSRF and XSS are both classic web attacks, both involve browsers and servers, and both appear on every security checklist — but they exploit fundamentally different trust relationships and require different mitigations.
XSS: The Attacker Injects Code Into Your Site
Cross-Site Scripting (XSS) happens when an attacker manages to inject malicious JavaScript into a page that other users then load. The browser executes it because it looks like legitimate code from your site.
There are three variants:
Stored XSS — the malicious script is saved in your database (e.g. a comment field) and served to every user who visits that page.
Reflected XSS — the script is embedded in a URL. When a victim clicks the link, the server reflects the input back in the response without sanitising it.
DOM-based XSS — the attack happens entirely client-side. JavaScript on your page reads from a source like location.hash or document.referrer and writes it to the DOM without sanitisation.
// ❌ DOM-based XSS vulnerability
const name = new URLSearchParams(location.search).get('name')
document.getElementById('greeting').innerHTML = `Hello, ${name}!`
// If name = <script>stealCookies()</script> — you're compromised
The fix is to never use innerHTML with untrusted input. Use textContent instead, or a sanitisation library like DOMPurify:
// ✅ Safe
document.getElementById('greeting').textContent = `Hello, ${name}!`
// ✅ Safe with HTML allowed (e.g. rich text)
import DOMPurify from 'dompurify'
el.innerHTML = DOMPurify.sanitize(userInput)
What XSS Can Do
Once an attacker's script runs in your origin, it has full access to everything in that context: cookies (unless HttpOnly), localStorage, sessionStorage, the DOM, and the ability to make authenticated requests or exfiltrate data to an external server. XSS is game over for the affected session.
CSRF: The Attacker Exploits Your Browser's Cookies
Cross-Site Request Forgery (CSRF) is a different attack entirely. The attacker doesn't inject code into your site. Instead, they trick the victim's browser into sending a forged request to your site from a different origin — and the browser helpfully attaches the victim's cookies.
<!-- Attacker's page at evil.com -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
When the victim loads this page, their browser fires a GET request to bank.com. Because the victim is logged in, their session cookie is included automatically. If bank.com performs state-changing operations on GET, the transfer goes through.
Modern browsers have reduced this risk significantly with SameSite cookies, but CSRF still matters for older browsers or cross-site flows that legitimately need cookies.
How to Defend Against CSRF
CSRF tokens are the classic defence. The server generates a random, unpredictable token per session (or per form), includes it in the HTML, and requires it on every state-changing request. An attacker on another origin can't read your page's content (same-origin policy blocks it), so they can't obtain the token.
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="a3f9...token...d2c1">
...
</form>
SameSite cookies are the modern, lower-friction solution. Setting SameSite=Strict means the cookie is never sent on cross-site requests at all. SameSite=Lax (the browser default since ~2020) allows cookies on top-level navigations but blocks them on subresource requests from other origins.
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
Origin / Referer header validation — check that the request's Origin or Referer header matches your domain. Not bulletproof on its own (some browsers omit headers), but a good second layer.
Side-by-Side Comparison
| XSS | CSRF | |
|---|---|---|
| Attack vector | Injecting code into your site | Forging requests from another site |
| Trust exploited | User trusts your site's scripts | Server trusts the user's browser/cookies |
| Attacker needs | A way to inject unsanitised input | A logged-in victim to visit attacker's page |
| Primary defences | Input sanitisation, CSP | CSRF tokens, SameSite cookies |
Cookie HttpOnly helps? | Yes — prevents JS from reading cookies | No — cookies are still sent automatically |
Content Security Policy Reduces XSS Blast Radius
A Content Security Policy (CSP) header tells the browser which sources of scripts are trusted. Even if an attacker injects a <script> tag, it won't execute if the source isn't whitelisted:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.com
CSP doesn't eliminate XSS (it's a mitigation, not a prevention) but it meaningfully reduces what an attacker can do with a successful injection.
Conclusion
XSS and CSRF target opposite ends of the trust model. XSS breaks the user's trust in your site's content by injecting code. CSRF breaks your server's trust in the user's browser by forging requests. Defending against both is non-negotiable: sanitise all output, use HttpOnly + SameSite cookies, issue CSRF tokens for state-changing endpoints, and add a Content Security Policy as a defence-in-depth layer.
