dissected.io
Lesson 9 of 15
Intermediateintermediatecsrfsamesitecookiesweb-security10 min read

CSRF Attacks

The browser's most helpful behavior is also its most dangerous: it attaches cookies to every request to a domain, regardless of where that request originates. If you're logged into bank.com, every request your browser makes to bank.com carries your session cookie — even if the request was triggered by a page on evil.com. CSRF exploits exactly this.

CSRF Attack — Auto-Attached Cookie

evil.com

Victim Browser

bank.com

CSRF exploits the browser auto-attaching cookies to every request for a domain

How the Attack Works

The attacker doesn't need to read your data. They don't need your cookies. They just need you to perform an action while you're logged in.

Here's the classic scenario. You're authenticated on bank.com. You visit a page on evil.com (maybe a forum post, a phishing link, or a compromised ad). That page contains:

<form action="https://bank.com/transfer" method="POST" id="f">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('f').submit();</script>

The moment the page loads, it auto-submits the form. Your browser dutifully sends the POST request to bank.com with your session cookie attached. The bank's server sees a valid authenticated request and processes the transfer. You never clicked anything, never saw a confirmation.

Why the Attacker Doesn't Need the Response

The same-origin policy prevents evil.com from reading the response from bank.com. The attacker can't see your account balance, your transaction history, or the transfer confirmation. But they don't need to. The action already happened. CSRF is a write-only attack — the attacker makes you do something, they don't steal your data.

This is why CSRF is fundamentally different from XSS. XSS gives the attacker full read-write access within your session. CSRF gives them write-only, and only for actions they can predict the parameters for (since they can't read forms or tokens from the target site).

Why XSS Defeats Every CSRF Defense

This is worth stating clearly: if your site has an XSS vulnerability, every CSRF defense is useless. An XSS payload runs JavaScript in the target origin, meaning it can read CSRF tokens from forms, read cookie values (if not HttpOnly), and submit requests that look perfectly legitimate. XSS is strictly more powerful than CSRF. Fix XSS first.

Defenses

SameSite Cookies

The biggest shift in CSRF defense happened when browsers made SameSite=Lax the default for cookies. With Lax, the browser only sends cookies on cross-site requests if the request is a top-level navigation using a safe method (GET). Cross-site POST requests, iframe loads, and AJAX calls don't get cookies attached.

Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly

SameSite=Strict goes further — cookies are never sent on any cross-site request, even top-level GET navigations. This is more secure but breaks legitimate flows (clicking a link to your site from an email won't have the user logged in).

SameSite=None opts out of protection entirely and requires the Secure flag. Only use it when you genuinely need cross-site cookie sending (embedded widgets, OAuth flows).

Synchronizer Token Pattern

The classic server-side defense. The server generates a random, unpredictable token and embeds it in every form as a hidden field. When the form is submitted, the server checks that the token matches the one stored in the user's session.

<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="a8f2e9..." />
  <!-- other fields -->
</form>

The attacker on evil.com can't read this token (same-origin policy prevents it), so they can't include it in their forged request. The server rejects any request without a valid token.

Most frameworks generate and validate these tokens automatically. Django has {% csrf_token %}, Rails has authenticity_token, Spring has CsrfFilter. If your framework provides it, use it.

An alternative when server-side state for tokens is inconvenient. The server sets a random value in both a cookie and a request parameter (or custom header). On submission, the server checks that the cookie value matches the parameter value.

The attacker can't read or set cookies for the target domain from their origin, so they can't construct a matching pair. This works without server-side session storage, making it useful for stateless architectures.

Origin and Referer Header Validation

The server can check the Origin or Referer header on incoming requests. If the origin doesn't match the expected domain, reject the request.

This is a reasonable supplementary check but shouldn't be your only defense. Some browsers, proxies, and privacy extensions strip or modify these headers. Edge cases exist with redirects and data URIs. Use it alongside other defenses.

Re-authentication for Sensitive Actions

For high-value operations — changing passwords, transferring money, modifying account settings — require the user to re-enter their password or complete a second factor. Even if CSRF somehow bypasses token validation, the attacker can't provide the user's current password.

Why CORS Is Not a CSRF Defense

This is a common misconception. CORS (Cross-Origin Resource Sharing) controls whether the browser allows JavaScript to read the response from a cross-origin request. It does not prevent the request from being sent.

A CSRF attack doesn't need to read the response. The form submission or image load fires the request, the cookies go along, and the server processes it. CORS never enters the picture for simple requests (form submissions are simple requests). Even for preflighted requests, CORS restricts the browser from reading the response — the preflight OPTIONS request is a browser-side check that the server can opt into, but it doesn't stop all requests.

Where CSRF Still Bites

SameSite=Lax as the browser default massively reduced CSRF. Most modern applications are protected without doing anything. But there are still exploitable gaps:

GET requests that change state. If your application performs actions on GET requests (like /delete?id=5 or /toggle-admin?user=3), SameSite=Lax doesn't help because Lax allows cookies on top-level GET navigations. This is why GET should always be safe and idempotent.

Same-site subdomain attacks. SameSite is based on the registrable domain, not the exact origin. If an attacker controls evil.example.com, requests from there to app.example.com are considered same-site, and cookies flow freely. Compromised subdomains — a staging environment, a user-content subdomain, a forgotten marketing site — can be used to CSRF the main application.

Older browsers. Some older browsers don't support SameSite or don't default to Lax. If your user base includes these browsers, you still need token-based defenses.

Misconfigured SameSite=None. Some applications explicitly set SameSite=None for convenience or because of third-party embedding requirements, unwittingly removing the protection.

Key Takeaways

  • CSRF works because browsers attach cookies to every request to a domain, regardless of which site initiated the request — the attacker makes you perform an action, they don't read your data.
  • SameSite=Lax as the browser default has dramatically reduced CSRF risk, but it doesn't cover GET-based state changes or same-site subdomain attacks.
  • The synchronizer token pattern remains the gold standard for server-side CSRF protection, and most frameworks implement it out of the box.
  • CORS is not a CSRF defense — it controls response reading, not request sending.
  • XSS defeats every CSRF defense because it runs in the target origin and can read tokens directly.
  • Defense in depth means combining SameSite cookies, CSRF tokens, origin validation, and re-authentication for sensitive actions.

Next up: Penetration Testing Basics — learning to think like an attacker and systematically finding vulnerabilities before they find you.

Enjoyed this breakdown?

Get new lessons in your inbox.