Library
8 min read

CORS vs CSRF

Published

These two get mixed up because both involve one website talking to another, and both show up as errors in the browser console. They are opposites in a useful way. CORS is a permission system: your server tells the browser which other sites may read its responses. CSRF is an attack: another site makes your browser send a request to your server, with your cookies attached, and the server cannot tell you did not mean it.

Reading versus sending. Keep those two words and the rest follows.

Your answer

Your bank's API has no CORS headers at all, so the browser blocks every other site from reading its responses. Is the bank protected from an evil site that submits a hidden form to /transfer?

The rule underneath both: same origin#

An origin is scheme, host and port: https://app.acme.in:443. Change any one and it is a different origin. The browser's same origin policy says a page's scripts may freely talk to their own origin, but may not read responses from another origin.

Notice the word read. The same origin policy was never about sending. Since the 1990s, any page has been able to send requests anywhere: an <img> loads from another site, a <form> posts to another site. Blocking that would break the web. And when the browser sends a request to a site, it attaches that site's cookies. That combination is where CSRF comes from.

CORS: opening the read door, carefully#

Your frontend at app.acme.in calls your API at api.acme.in. Different origins, so the browser blocks the script from reading the response. Cross Origin Resource Sharing lets the API say "I allow this":

http
Access-Control-Allow-Origin: https://app.acme.in
Access-Control-Allow-Credentials: true
Vary: Origin

For requests that could not have come from a plain HTML form (a JSON body, a PUT, a custom header like Authorization), the browser first sends a preflight OPTIONS request asking permission, and only sends the real request if the answer allows it.

1/6 Not a "simple" request, so the browser must check first.

Two rules that come up in every code review. Access-Control-Allow-Origin: * cannot be used with credentials; browsers refuse it. And reflecting whatever Origin the request carries back into that header, with credentials allowed, is the same as allowing every site on the internet to read your users' data. It is one of the most common API misconfigurations security scanners find.

CSRF: the request you did not mean to send#

You are logged in to your bank in one tab. In another, you open a funny meme page. It contains:

html
<form action="https://bank.example/transfer" method="POST" id="f">
  <input name="to" value="attacker-account" />
  <input name="amount" value="50000" />
</form>
<script>document.getElementById('f').submit()</script>

Your browser sends that POST to the bank and attaches your bank session cookie, because it is a request to the bank. The bank sees a valid session and moves the money. The attacker never saw your cookie and never needed to read the response. That is cross site request forgery. CORS is irrelevant here: a form POST is a "simple" request with no preflight, and the damage is done by the time anyone thinks about reading the reply.

Play with the three defences and see what each one actually stops.

You are signed in to the bank. Now open evil.example
What evil.example does
Session cookie SameSite
CORS allows evil.example
CSRF token required
Request sent (no preflight for this kind)
Session cookie attached
Bank treated it as you
₹50,000 moved to the attacker
This is CSRF. The browser attached your session cookie to a request evil.example wrote, and nothing else checked where it came from. CORS did not help: the request went out, and the attacker never needed to read the reply.

Defending against CSRF#

SameSite cookies. The SameSite attribute tells the browser when to attach a cookie to a request started by another site. Strict: never. Lax: only on top level navigations with safe methods, like clicking a link (so a cross site form POST goes without it). None: always, and it must also be Secure. Chrome treats cookies with no SameSite attribute as Lax by default since 2020, which quietly killed a lot of classic CSRF. Set it explicitly anyway, since not every browser applies that default.

CSRF tokens. The server puts a random value in each form or page, tied to the session, and requires it back on every state changing request. The attacker's page cannot read your bank's pages (the same origin policy), so it cannot know the token.

Check where the request came from. Modern browsers send Origin and Sec-Fetch-Site headers that pages cannot forge. Rejecting state changing requests where Sec-Fetch-Site is cross-site is a short, effective rule.

Do not use cookies for API auth. A token in an Authorization header is not attached automatically, so there is nothing for another site to ride on. That swaps CSRF risk for XSS risk, which is the subject of cookies vs localStorage.

ts
// Reject state changing requests that another site started.
app.use((req, res, next) => {
  const safe = ['GET', 'HEAD', 'OPTIONS'].includes(req.method)
  const site = req.get('Sec-Fetch-Site') // same-origin | same-site | cross-site | none
  if (!safe && site === 'cross-site') return res.status(403).send('cross site request blocked')
  next()
})
ChoiceWhat you gainWhat you payPick it when
CORSLets chosen origins read your API responses from the browser.Does not stop requests from being sent, so it is not a CSRF defence. Loose configs leak data.Whenever a frontend calls an API on another origin.
SameSite=Lax or StrictBrowser keeps the session cookie off cross site requests. One attribute, broad protection.Strict breaks inbound links (you arrive logged out). Same site subdomains still count as same site.Every session cookie. Lax is the usual choice.
CSRF tokensWorks regardless of browser cookie behaviour.Every form and AJAX call must carry it. Undone by an XSS or a careless CORS policy.Cookie authenticated apps, as defence in depth.
Origin / Sec-Fetch-Site checksA few lines of middleware, no state.Very old browsers do not send Sec-Fetch headers.A cheap extra layer on every state changing endpoint.

In the interview#

Interviewer
Does CORS protect against CSRF?
The most common mix up in web security interviews.
You
No. CORS controls whether a script on another origin may read the response. CSRF works by making the browser send a request with the victim's cookies, and a form POST or an image tag sends without any CORS check. The server executes it before CORS is even consulted. The one partial exception is requests that need a preflight, like a JSON body, but you cannot rely on every endpoint requiring one. The defences are SameSite cookies, CSRF tokens and checking Origin or Sec-Fetch-Site.
Reading versus sending, plus the preflight nuance, plus the real fixes.
Interviewer
Our API only accepts a bearer token in the Authorization header. Do we need CSRF protection?
You
Not for that API, since the browser never attaches the token automatically. But the token now lives somewhere JavaScript can read it, so the risk moves to XSS. And if any endpoint still accepts a cookie, it needs CSRF protection.
CORS problem, CSRF problem, or neither?
Console: "blocked by CORS policy: No Access-Control-Allow-Origin header"
A hidden form on another site changes a user's email address
API reflects any Origin and allows credentials
GET /logout via an image tag on another site logs users out

Checkpoint#

Checkpoint

1. The bank sends no CORS headers. An evil page auto submits a form to /transfer. What happens if the session cookie is SameSite=None and there is no CSRF token?

2. Why can't an attacker's page just read the CSRF token from your site?

3. Which header combination should never appear together?

Worth memorising
  • CORS: who may read. CSRF: tricking the browser into sending.
  • The same origin policy blocks reading, never sending.
  • Preflight happens for non simple requests: JSON bodies, PUT and DELETE, custom headers.
  • CSRF defences: SameSite, CSRF tokens, Origin and Sec-Fetch-Site checks.
  • Never reflect Origin with credentials. Allowlist exact origins.
Say this in 60 seconds

CORS and CSRF are about reading versus sending. The same origin policy stops a script from reading responses from another origin, but it never stopped sending: forms and images have always crossed origins, and the browser attaches the target site's cookies. CORS is how a server opts in to letting specific other origins read its responses, with a preflight OPTIONS request for anything a plain form could not send. CSRF is the attack where another site makes your browser send a state changing request with your cookies. CORS does not stop it, because the request executes before any response is read. The defences are SameSite cookies, Lax by default, CSRF tokens that other origins cannot read, checking Origin or Sec-Fetch-Site on state changing requests, and keeping GETs side effect free. And a loose CORS policy that reflects any origin with credentials undoes the token defence.