You're logged into your bank. The session is active, the authentication cookie is sitting in your browser. You open another tab and visit an unrelated website. That website causes your browser to send a POST request to your bank.
Your browser attaches the bank's authentication cookie automatically. The bank receives what looks like an authenticated request. It processes it.
You didn't initiate that request. But your browser did, using your credentials.
That's Cross-Site Request Forgery. The name describes the mechanism precisely: a request is forged, it's cross-site in origin, and it exploits the fact that your browser carries your credentials wherever it goes.
Why the Browser Does This
Cookies are scoped to domains. When your browser makes a request to a domain, it includes that domain's eligible cookies automatically. This is normal behavior, not a bug. It's what keeps you logged into websites as you navigate around.
The browser doesn't distinguish between:
- a request you consciously initiated by clicking a button
- a request triggered by JavaScript on a page you're viewing
- a request caused by a resource on another website From the browser's perspective, a request to bank.com gets bank.com's cookies. The origin of the request, which page caused it, is not reflected in those credentials.
The server receives the HTTP request and sees valid authentication. It has no way to know, based on the cookie alone, whether you meant to send that request.
Legitimate request:
Browser
↓
POST /change-email + authentication cookie
↓
Application
CSRF scenario:
Other website
↓
Triggers request from victim's browser
↓
POST /change-email + authentication cookie
↓
Application (sees valid credentials)
The attacker doesn't need to steal the cookie. They don't need to read the response. The goal is simply to cause a state-changing request to happen using the victim's existing session.
Authentication Is Not the Same as Intent
This is the core of the problem.
Authentication tells the server who is making the request. A valid cookie establishes that the request comes from an authenticated session. But authentication says nothing about whether the user actually intended to perform the action.
When a server receives a POST to /transfer with a valid authentication cookie, it knows the request is associated with a logged-in account. It does not know whether the account holder clicked a button, or whether another website silently triggered that request in the background.
CSRF exploits the gap between those two things. The credentials are genuine. The request was not.
CSRF Tokens
The standard defense against CSRF is introducing something the attacker cannot obtain: an unpredictable token tied to the user's session.
<form action="/change-email" method="POST">
<input type="hidden" name="csrf_token" value="k9Zm3...">
<input type="text" name="email">
<button type="submit">Update Email</button>
</form>
The server generates this token and associates it with the user's session or request context. The legitimate application page includes the token in the form. When the form is submitted, the server checks that the token is present and valid before processing the request.
The reason this works comes down to browser same-origin restrictions. A page on another domain cannot read the content of your application's pages. It can cause your browser to send a request to the application, but it cannot read the token embedded in the form. Without the token, the forged request fails the server's check.
Implementations vary. The classic approach stores a token in the session and verifies it server-side. Stateless patterns like signed double-submit cookies avoid server-side state entirely. Both separate "authenticated request" from "request carrying evidence it came from our own page."
SameSite Cookies
Modern browsers support a SameSite attribute on cookies that controls whether they're sent with cross-site requests.
SameSite=Strict prevents the cookie from being sent with any cross-site request, including navigations. Clicking a link from another site to your banking dashboard would not send the session cookie.
SameSite=Lax is more permissive. Cookies are sent with top-level navigation requests like clicking a link, but not with cross-site subresource requests like forms or fetches triggered by another page. Many browsers now use Lax as the default when SameSite is not explicitly set.
SameSite=None sends the cookie with all cross-site requests, which requires Secure and is used for contexts that genuinely need this behavior.
SameSite=Lax or Strict can significantly reduce CSRF risk because the browser simply won't attach the session cookie to forged cross-site requests. But SameSite is a browser-level mitigation. Relying on it alone without application-level token validation isn't a complete approach, both because browser behavior isn't fully uniform across every scenario and because defense in depth matters.
Origin and Referer Headers
Servers can also inspect the Origin and Referer headers to check where a request came from. A state-changing request arriving from an unexpected origin is a signal worth acting on.
These checks add another layer, but they come with caveats. Neither header is guaranteed to be present on every request. Referer can be stripped by privacy settings or browser behavior. These checks work best as part of a defense-in-depth approach alongside CSRF tokens and appropriate cookie settings, not as the sole defense.
CSRF and XSS Are Different Threats
Since XSS came up in the previous article, the comparison is worth making explicit.
XSS introduces attacker-controlled content into the victim's browser in the application's own origin, where it can run as part of the page. CSRF causes the victim's browser to make a request to a target application from a different origin, exploiting automatic credential attachment.
One important connection: XSS can undermine CSRF defenses within the affected application. If attacker-controlled JavaScript executes in the application's origin, it may be able to read CSRF tokens, make requests with those tokens attached, and bypass the protection entirely. Fixing XSS and CSRF vulnerabilities are separate efforts, but XSS in an application can render its CSRF mitigations ineffective.
Bearer Tokens and the CSRF Threat Model
APIs that require an explicit authorization header rather than a cookie work differently:
Authorization: Bearer <token>
The browser doesn't automatically attach authorization headers to cross-site requests the way it does with cookies. Another site can cause your browser to send a request, but it cannot cause that request to include a bearer token it doesn't have access to.
This changes the CSRF threat model. If authentication requires JavaScript to explicitly retrieve and attach a token that another origin cannot access, the traditional cookie-based CSRF attack doesn't apply in the same way.
But this doesn't make bearer tokens unconditionally safe from CSRF. Security properties depend on where the token is stored, how it's obtained, and the application's overall design. The broader point is that CSRF risk is closely tied to the authentication mechanism and what the browser supplies automatically.
CSRF works because browsers carry credentials on behalf of the user, and servers can't always tell the difference between a request the user intended and one they didn't.
A session cookie proves authentication. It doesn't prove intent. The defenses, CSRF tokens, SameSite cookies, origin checks, all exist to give the server something that proves the request actually originated from its own trusted context.
Authentication and intent are different things. CSRF is what happens when a server treats them as the same.
Top comments (0)