How HTTP Works

Comparison

CORS vs CSRF: Response Reads vs Forged Writes

CORS controls cross-origin response reads; CSRF abuses automatically sent credentials. Compare preflights, SameSite, tokens, Origin and Sec-Fetch-Site.

Bottom line: Configure CORS for cross-origin response access. Protect cookie-authenticated writes separately: a simple request can reach your server even when the browser hides its response.

By How HTTP WorksReview process
CORS
vs
CSRF Protection

TL;DR: CORS controls which other origins can read your responses. CSRF is an attack where another site makes the user’s browser send a write with their cookies attached. They solve different problems: a CORS error hides the response, but a simple cross-site POST still reaches your server and still runs. Protect cookie-authenticated writes with tokens, Origin checks, or Sec-Fetch-Site, whatever your CORS policy says.

The Core Difference

CORS is a browser protocol for sharing responses across origins. CSRF is an attack: another site gets the victim’s browser to send a request carrying credentials the browser attaches automatically, such as session cookies. The attacker never needs to read the reply. Changing the account email or triggering a transfer is the whole point.

DecisionCORSCSRF protection
What is controlledCross-origin response access; preflight permission for certain requestsWhether the server accepts an unwanted authenticated action
Where the check runsBrowser, using the server’s CORS response headersServer checks, aided by browser cookie and metadata rules
Relevant request dataOrigin; preflight method and header namesToken, Origin, Sec-Fetch-Site, credential context
Simple POST without CORS permissionCan be sent; response is not shared with the calling scriptMust be rejected before changing state if forged
Non-browser clientDoes not have to enforce CORSStill needs authentication and authorization

The read/preflight distinction is defined by the Fetch Standard’s CORS protocol. MDN’s CORS guide explains why form-compatible requests are permitted without preflight: servers must already defend against forged submissions.

Why The Write Can Happen Despite A CORS Error

This request needs no CORS preflight (the requests, headers, and URLs on this page are examples):

// Run from another origin in a controlled test against your own endpoint.
fetch('https://app.example.com/account/email', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: 'email=changed%40example.net'
})

If cookie rules allow the session cookie, the browser sends the authenticated POST and only checks CORS when the response comes back. A missing Access-Control-Allow-Origin makes the fetch fail for the calling script, but by then the server has already done the write. Cross-origin fetch needs credentials: 'include' to send cookies, and SameSite and third-party cookie policies apply on top of that (MDN Using Fetch).

A plain cross-site HTML form can submit the same POST. GET, HEAD and POST with only safelisted headers skip preflight. The safelisted content types include application/x-www-form-urlencoded, multipart/form-data and text/plain, subject to the header-value rules (Fetch Standard).

Content-Type: application/json or a custom X-CSRF-Token header is different: a cross-origin fetch with either one needs a preflight, and if you deny the preflight, the real request never goes out. You can build a defense on that, but only if the server requires the non-safelisted input on every write and rejects form-compatible versions. If your frontend sends JSON but the server also accepts form posts, attackers just use a form (MDN CSRF: non-simple requests).

What To Check Before Accepting A Write

SameSite cookies: SameSite=Lax keeps cookies off cross-site POSTs but still sends them on top-level safe navigations such as a GET link. Strict is tighter; None allows cross-site use and requires Secure. Browser third-party cookie restrictions may block the cookie independently (MDN Set-Cookie).

“Same-site” is broader than “same-origin”: https://app.example.com and https://uploads.example.com can be same-site. If a sibling subdomain hosts untrusted content, SameSite won’t protect you from it. And keep state changes off GET; RFC 9110 section 9.2.1 defines safe methods as essentially read-only.

CSRF token: let your framework’s CSRF protection generate and check an unpredictable token. Send it in a hidden form field or a request header, and validate it before changing anything. A token that only travels as a cookie proves nothing, because the browser attaches it to forged requests too (MDN CSRF tokens).

Origin: parse the origin and compare it exactly with your list of trusted origins, scheme and port included. A substring match on example.com would also accept https://example.com.attacker.example. Reject untrusted origins before changing anything. Decide up front what happens when Origin is missing or null, because both happen, and make sure that path still gets checked. MDN Origin documents those cases. For a browser-only write endpoint, requiring a valid token when Origin is unavailable is one explicit policy choice.

Sec-Fetch-Site: reject state-changing browser requests marked cross-site unless the endpoint is meant to accept them. Decide whether you trust sibling subdomains, which arrive as same-site, not same-origin. Browsers set Sec-* headers themselves, but non-browser clients can send anything, and some requests arrive without them. Fall back to a token or Origin check when the metadata is missing. See W3C Fetch Metadata and MDN Sec-Fetch-Site.

When Each Applies

CORS: a frontend at https://app.example.com needs to read a cookie-authenticated API at https://api.example.com. This response allows that one origin:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Send Vary: Origin whenever you pick the allowed origin per request. A credentialed response can’t use Access-Control-Allow-Origin: * to grant script access (MDN CORS). CORS permission doesn’t override SameSite, either.

CSRF protection: the same API accepts writes authenticated by its session cookie. Check each write request before updating the account, whether or not the response carries CORS headers. A same-origin web app needs CSRF defenses too, even though it never enables CORS for its own frontend.

The Common Mistake: Confusing A Hidden Response With A Rejected Request

A CORS error in the console tells you the script couldn’t read the response. It tells you nothing about whether the database changed. Check whether the real POST went out and whether the server rejected it before doing anything. If the preflight was denied, confirm that only the OPTIONS request reached the endpoint.

Check On The Wire

Use a test account on your own staging server. This checks the server-side policy; with an untrusted Origin, you want a rejection and no change:

curl -i https://app.example.com/account/email \
  -H 'Origin: https://attacker.example' \
  -H 'Sec-Fetch-Site: cross-site' \
  -b 'session=YOUR_TEST_SESSION_ID' \
  --data-urlencode 'email=changed@example.net'

curl sends exactly the headers and cookie you give it and ignores CORS entirely. In browser DevTools, look at the OPTIONS and POST requests separately, then check Request Headers for Origin, Sec-Fetch-Site, and whether the cookie went along. Try a form-compatible POST and a JSON POST from another origin, and check server state as well as the response status.

Browse /search