Join our Newsletter — 33% off our NHI Course
Home› Guides› CORS vs CSRF Protection: Why CORS Does Not…
Guide Web & API Security

CORS vs CSRF Protection: Why CORS Does Not Stop Forged Requests

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 17 min read
On this page

A strict CORS policy does not protect a cookie-authenticated application from cross-site request forgery, yet "we locked down CORS" is still one of the most common answers given when an assessor asks about CSRF. CORS decides whether a script on another origin may read a response, while CSRF is about whether a forged request is processed at all, and the forged request is usually sent before CORS has any say. This guide explains exactly where the misconception breaks, which requests skip preflight, when preflight genuinely helps, and what Django, Express and Go applications should configure instead. It is for application security engineers, developers and assessors.

Key takeaways

  • CORS controls reading, not sending. A simple cross-origin request reaches your server and runs your handler; the browser only withholds the response from the calling script afterwards.
  • Forms never ask CORS for permission. An HTML form posting form-encoded, multipart or text/plain data to your site is a simple request, so no preflight, no allowlist and no CORS error stops it.
  • Preflight can be a CSRF barrier, but only if nothing bypasses it. OWASP ASVS 5.0 V3.5.2 requires that sensitive functions protected this way cannot be called with a request that skips preflight.
  • A loose CORS policy makes CSRF worse. Reflecting any Origin with credentials lets an attacker's page read your responses, including the anti-forgery tokens meant to stop it.
  • The real defences are server side. Framework tokens, Fetch Metadata with Origin verification, explicit SameSite cookies and no state change on GET do the work; CORS hygiene keeps those defences intact.

The short answer

Does CORS prevent CSRF? No. PortSwigger's Web Security Academy states that "CORS is not a protection against cross-origin attacks such as cross-site request forgery (CSRF)". MDN's CORS guide explains why a server does not have to opt in to receive a simple request: the threat of CSRF from such a request is no worse than that of an ordinary form submission, which browsers have always allowed. The server opts in only to share the response with a script. The django-cors-headers documentation puts it in four words: "CORS and CSRF are separate."

The same-origin policy already stops other pages reading your responses, and CORS only relaxes it. CSRF never needed to read anything.

There is one narrow case where CORS machinery does help. A non-simple request, such as a POST with Content-Type: application/json or a custom header like X-Requested-With, triggers a preflight OPTIONS request first, and the browser will not send the real request unless your preflight response allows the calling origin. If, and only if, your endpoint rejects every request that could have been sent without a preflight, the preflight acts as a CSRF control. That condition is easy to break, as later sections show. For the wider background on the browser model, see the Same-Origin Policy and Browser Security Model Guide.

Side by side

DimensionCORSCSRF protection
Question it answersMay a script on origin A read this response from origin B?Did the user intentionally send this state-changing request from our own application?
Who enforces itThe browser, using headers the server returnsThe server, using a signal only its own pages can produce
Default when absentMost restrictive: cross-origin scripts cannot read responsesLeast restrictive: any site can make the browser send cookie-bearing requests, subject to SameSite
When the decision is madeAfter the response arrives (simple requests) or before sending (preflighted requests)Before the handler changes state
Main mechanismsAccess-Control-Allow-Origin, -Allow-Credentials, -Allow-Methods, -Allow-Headers, -Max-Age, Vary: OriginSynchroniser or signed double-submit tokens, Sec-Fetch-Site, Origin and Referer checks, SameSite cookies, custom headers
Typical weaknessCWE-942: permissive cross-domain policy, such as reflected Origin with credentialsCWE-352: cross-site request forgery, an unprotected state-changing endpoint
Effect of non-browser clientsNone: curl and server-side code ignore CORS entirelyNot the threat: CSRF needs a victim's browser to attach ambient credentials
ASVS 5.0 requirementsV3.4.2 (allowlisted or fixed Access-Control-Allow-Origin)V3.5.1, V3.5.2, V3.5.3 (anti-forgery, preflight reliance, safe methods), V3.3.2 (SameSite)

The "default when absent" row is the heart of the misconception. A missing CORS policy is safe; a missing CSRF defence is not. An API with no CORS headers is closed to reading by other sites and open to being written to by them.

Simple requests and preflight: the mechanics that matter

MDN defines a simple request as one that meets all of these conditions. Anything else is preflighted.

ConditionAllowed in a simple requestCSRF implication
MethodGET, HEAD, POSTPOST state changes are reachable without preflight; PUT, PATCH and DELETE from a browser always preflight
Headers set by the pageAccept, Accept-Language, Content-Language, Content-Type (restricted), Range (single range)Any other header, such as X-CSRF-Token or Authorization, forces a preflight
Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plainAn HTML form can send all three; a JSON body labelled text/plain is still simple

Simple request flow: the browser sends the request immediately, with cookies if the caller asked for credentials and SameSite allows them. Your server processes it. Only when the response comes back does the browser check Access-Control-Allow-Origin and Access-Control-Allow-Credentials. MDN notes that a credentialed simple GET "is not preflighted" and that the browser will reject a response lacking Access-Control-Allow-Credentials: true by not making it available to the page. The side effect has already happened.

Preflight flow: the browser first sends OPTIONS with Origin, Access-Control-Request-Method and Access-Control-Request-Headers, and without credentials (MDN: preflight requests "must never include credentials"). Only if the response allows the origin, method and headers does the real request go out. The result may be cached for Access-Control-Max-Age seconds, which MDN gives a default of 5 seconds, each browser applying its own ceiling.

HTML forms and navigations are not CORS requests at all: no preflight, no response check, and every classic CSRF attack uses one.

CORS: threat model and architecture

Threat model: CORS enables legitimate cross-origin reads. The threat it introduces is a malicious page reading authenticated data through a victim's browser from an origin that trusts it too broadly. It does not model forged writes.

Architecture in words: the server either returns no CORS headers (safe default), returns a fixed origin, or compares the request's Origin header with an allowlist and echoes it back. For credentialed requests the server must name an explicit origin and send Access-Control-Allow-Credentials: true; MDN states that a wildcard in Access-Control-Allow-Origin, -Allow-Headers, -Allow-Methods or -Expose-Headers is not allowed with credentials, and any Set-Cookie on such a response is ignored.

How CORS undermines CSRF defences when it is wrong: PortSwigger explains that a server that reflects any Origin and allows credentials lets an external site read authenticated responses, exposing values such as API keys or CSRF tokens. Once an attacker's script can read a page that contains your anti-forgery token, it can fetch the token and submit a valid forged request. The same applies to an allowlisted origin with XSS, an allowlisted null origin and unanchored pattern matches. See the CORS Misconfiguration Guide for the full catalogue of these failures and their fixes.

CSRF-specific failure modes:

  • Relying on preflight while accepting simple content types: the JSON endpoint also parses text/plain or form bodies, so an attacker sends one of those and no preflight occurs.
  • Allowing credentials to a broad or reflected origin set: converts a blind CSRF attacker into one who can read tokens and responses.
  • Regex subdomain allowlists: OWASP warns that an attacker who takes over a subdomain can then send your custom header with full preflight approval. See the Subdomain Takeover and Dangling DNS Guide.

CSRF protection: threat model and architecture

Threat model: an attacker who can get a signed-in victim to load content they control, and who knows the parameters of a state-changing endpoint. They cannot read responses unless CORS or XSS lets them. The attack depends on credentials the browser attaches automatically, which means cookies and HTTP authentication. Script-added bearer tokens are not. See Session Cookies vs Bearer Tokens for that trade-off.

Architecture in words: before any unsafe method changes state, the server checks for a signal the attacker's page cannot produce. The OWASP CSRF Prevention Cheat Sheet ranks these as follows.

  • Fetch Metadata: OWASP calls Sec-Fetch-Site "the primary signal for CSRF protection". Treat cross-site as untrusted for state-changing methods, allow same-origin, and treat same-site conservatively unless every sibling subdomain is trusted. MDN marks the header Baseline widely available since March 2023, and because it carries the Sec- prefix it is a forbidden header that page script cannot set.
  • Origin verification: OWASP says verifying Origin, then Referer, "is a mandatory requirement" as the fallback for clients that omit Fetch Metadata. Compare the full serialised origin, and treat a missing header or Origin: null as unproven, never as same-origin.
  • Tokens: synchroniser tokens, or the signed double-submit pattern bound to the session with HMAC-SHA-256 and compared in constant time. OWASP calls naive double submit, where only the cookie and parameter are compared, insecure, because anyone who can write a cookie to your domain can forge a matching pair.
  • Custom request headers: OWASP notes this defence "relies on the CORS preflight mechanism". It is valid for API-only endpoints whose CORS policy is tight, and it fails silently if CORS is loosened later.
  • SameSite cookies: defence in depth. MDN explains that Lax sends cookies on cross-site requests only for top-level navigations with safe methods. OWASP notes that Chromium treats a cookie with no SameSite attribute as Lax while Firefox and Safari do not, that Lax cookies may still travel on cross-site top-level POSTs for about two minutes after being set, and that "if any state-changing operation in the application is reachable via a GET request, SameSite=Lax will not stop it."

Failure modes: GET endpoints that change state, blanket exemptions for API routes that still use cookies, unprotected login forms (login CSRF, which OWASP notes most developers ignore),, and treating same-site requests as trusted when a sibling subdomain hosts user content or has been taken over. Our CSRF Prevention Guide covers each defence in depth; this page focuses on how they interact with CORS.

Where the two meet: JSON APIs, preflight reliance and credentials

Most real confusion arises with JSON APIs behind cookie sessions. Three designs are common.

API designIs CSRF possible?What CORS contributesRequired control
Bearer token in Authorization header, no cookiesNo classic CSRF: the browser never attaches the token cross-siteNothing for CSRF; misconfiguration can still leak data to scripts that hold their own tokenProtect the token from XSS; keep CORS to known origins
Cookie session, strict application/json only, rejects other content types, custom header requiredOnly if CORS approves an attacker origin or a trusted origin is compromisedPreflight is the barrier; the allowlist is now a security controlASVS V3.5.2: verify Content-Type and Origin server side, plus Sec-Fetch-Site; keep the allowlist exact
Cookie session, JSON parser also accepts text/plain or form bodiesYes: an attacker sends a simple request with a JSON-shaped bodyNothing: the request is never preflightedTokens or Fetch Metadata, and reject simple content types on state-changing routes

OWASP warns specifically that JSON APIs "may accept text/plain which would be vulnerable to CSRF". A lenient body parser or a widened content-type list quietly removes the preflight barrier. ASVS 5.0 V3.5.1 states the general rule: if an application does not rely on preflight, it must validate that requests originate from the application, using anti-forgery tokens or extra headers that are not CORS-safelisted. V3.5.3 adds that sensitive functions must use methods such as POST, PUT, PATCH or DELETE, or strict validation of Sec-Fetch-* headers.

The combination of preflight reliance and credentialed CORS deserves special care. Once you rely on preflight, every origin on your credentialed allowlist can drive state changes in your API with a victim's cookies. That allowlist becomes part of your authorisation model and should be reviewed like one. See CSP vs CORS for protecting those trusted origins with a strict Content Security Policy.

Framework settings: Django, Express and Go

The pattern across frameworks is the same: CORS and CSRF are configured in different places, and allowing an origin in one does not allow it in the other.

FrameworkCSRF setting or componentCORS setting or componentWatch for
DjangoCsrfViewMiddleware with {% csrf_token %}; Origin checked against the host and CSRF_TRUSTED_ORIGINS; Referer checked on HTTPS when Origin is absent; header name via CSRF_HEADER_NAMEdjango-cors-headers: CORS_ALLOWED_ORIGINS, CORS_ALLOWED_ORIGIN_REGEXES, CORS_ALLOW_CREDENTIALS (default False), CORS_ALLOW_ALL_ORIGINS (default False), CORS_URLS_REGEX@csrf_exempt on views that use session cookies; regexes that match more than intended; Referrer-Policy: no-referrer, which Django notes breaks unsafe HTTPS requests
ExpressNo built-in protection; the long-used csurf repository was archived by its owner on 14 May 2025 and is no longer maintained, so use a maintained middleware or a Sec-Fetch-Site and Origin checkcors package: origin accepts a string, array, RegExp, function or true; credentials: true adds Access-Control-Allow-Credentialscors() with no options sends Access-Control-Allow-Origin: *; origin: true reflects the request Origin, unsafe with credentials
Go (net/http)http.CrossOriginProtection (Go 1.25): rejects non-safe cross-origin browser requests using Sec-Fetch-Site, falling back to comparing Origin with Host; AddTrustedOrigin for exact originsSet headers yourself or with a library; the standard library has no CORS handlerRequests with neither Sec-Fetch-Site nor Origin are allowed as likely same-origin or non-browser; AddInsecureBypassPattern exempts whole routes

Django in practice: the django-cors-headers documentation shows an origin that needs to read and write listed in both CORS_ALLOWED_ORIGINS and CSRF_TRUSTED_ORIGINS, and a read-only origin only in the first. Place CorsMiddleware as high as possible in MIDDLEWARE. Django's own documentation warns that any subdomain can set cookies for the whole domain and so bypass the token check, so give subdomains only to trusted parties. More hardening sits in the Django Security Guide.

Express in practice: pass an exact origin array, set credentials only where cookies are needed, scope cors() to the routes that need it, and add CSRF protection separately. See the Express.js Security Guide.

Worked scenario

A company runs a server-rendered customer portal at portal.example.com and a JSON API at api.example.com, both using the same session cookie scoped to .example.com with SameSite=Lax. Asked for CSRF evidence, the team cites the API's CORS allowlist and Django's CSRF middleware.

The assessor finds four problems.

  1. The API accepts text/plain. Its body parser reads JSON from any content type. A page on another site can submit a form with enctype text/plain whose body is valid JSON, so the request skips preflight. Because the cookie is explicitly Lax, a cross-site POST does not carry it today, but the team had been relying on that browser behaviour without knowing it, and an earlier configuration with no SameSite attribute would have left Firefox and Safari users exposed. Fix: reject anything except application/json on state-changing routes and add a Sec-Fetch-Site check.
  2. A legacy endpoint changes the notification email on GET. Lax permits cookies on top-level GET navigations, so a link is enough. Fix: POST only, plus re-authentication for email changes.
  3. The CORS allowlist is a regex, ^https://.*\.example\.com$. A forgotten subdomain points at a deprovisioned cloud host. Fix: an exact list, and a dangling DNS sweep.
  4. The portal views that call the API with fetch() carry @csrf_exempt. A developer added it to silence 403s from AJAX calls. Fix: send the token in the X-CSRFToken header and remove the exemption.

The CORS allowlist was doing none of the work the team believed; after the fixes it goes back to deciding who may read responses. The scenario is illustrative.

Decision criteria

  • Cookie-authenticated, server-rendered app: framework CSRF protection on every unsafe route, plus a Sec-Fetch-Site check. CORS is usually unnecessary; leave it unset.
  • Cookie-authenticated SPA calling its own API on a different origin: exact credentialed CORS allowlist, Sec-Fetch-Site with Origin fallback, and either tokens or strict JSON plus a custom header with simple content types rejected. Consider a backend-for-frontend on the same origin; see the OAuth for Browser-Based Applications and the BFF Pattern Guide.
  • Bearer-token API with no cookies: CSRF risk is low; CORS stays narrow to protect data. Keep it that way: adding a cookie later reintroduces CSRF.
  • Public, unauthenticated data: Access-Control-Allow-Origin: * is acceptable, as ASVS V3.4.2 allows, provided the response contains nothing sensitive and the host is not reachable only because of network position.
  • Small organisations: rely on framework defaults, never exempt routes without review, and set SameSite explicitly. Large organisations: centralise Fetch Metadata policy in shared middleware and keep CORS allowlists in reviewed configuration.

Rolling out and testing

  1. Inventory: list every state-changing endpoint, its authentication method and CORS headers, and search for csrf_exempt, route-level skips, AddInsecureBypassPattern and state-changing GET handlers.
  2. Test simple requests: for each cookie-authenticated endpoint, confirm it rejects a cross-site form-encoded, multipart and text/plain submission. Run these tests only in an authorised test environment.
  3. Deploy Fetch Metadata in log-only mode: record what a cross-site rejection would block for one or two release cycles, add trusted origins where genuine integrations appear, then enforce.
  4. Tighten CORS: replace reflection and regexes with exact lists; add Vary: Origin where the header is computed; remove credentials where no cookie is needed.
  5. Roll back by returning Fetch Metadata to log-only, never by disabling token checks.

Detection and measurement

  • Rejection logs: token, Origin and Sec-Fetch-Site failures by endpoint; a spike on one endpoint suggests probing.
  • CORS decisions: log Origin values that were echoed with credentials; any unexpected origin is an incident.

Metrics:

  • Percentage of cookie-authenticated, state-changing endpoints with verified server-side CSRF protection (target 100%).
  • Number of CSRF exemptions, each with an owner and justification, trending down.
  • Number of credentialed CORS origins per API, and the number matched by pattern rather than exact value (target zero).
  • State-changing GET handlers found in code or tests (target zero).
  • Percentage of session cookies with an explicit SameSite attribute.

Framework mapping

ConcernNIST SP 800-53 Rev. 5NIST CSF 2.0OWASP ASVS 5.0
Session integrity against forged requestsSC-23 Session AuthenticityPR.PS-06V3.5.1, V3.5.3
Preflight reliance and request validationSI-10 Information Input ValidationPR.PS-06V3.5.2
Cross-origin read policy and SameSite cookiesSC-23 Session AuthenticityPR.PS-06V3.4.2, V3.3.2
Testing both controlsSA-11 Developer Testing and EvaluationPR.PS-06V3.5.1, V3.5.2
Monitoring rejections and origin decisionsSI-4 System MonitoringDE.CM-09

In ISO/IEC 27001:2022 Annex A the closest controls are 8.26 Application security requirements, 8.28 Secure coding and 8.29 Security testing in development and acceptance. The underlying weaknesses are CWE-352 and CWE-942. Our OWASP ASVS Guide covers the wider standard.

Common failure patterns

  • Answering a CSRF question with a CORS policy: the policy governs reading, and every form-shaped write still reaches the handler.
  • Loosening CORS to fix a console error: reflecting Origin with credentials exposes tokens and turns blind CSRF into readable account takeover.
  • Preflight reliance without content-type enforcement: one parser change makes a "JSON-only" endpoint accept simple requests.
  • Exempting the API from CSRF while it still accepts the session cookie: common in Django and Express apps that add an SPA to a server-rendered site.
  • Trusting default SameSite behaviour: it differs by browser, includes a two-minute POST window and never protects state-changing GETs.

Practitioner checklist

  • Never cite CORS as CSRF evidence; record the server-side control for each state-changing route.
  • Enable framework CSRF protection globally and review every exemption.
  • Add a Sec-Fetch-Site check for unsafe methods, with Origin verification as the fallback.
  • Reject simple content types on JSON endpoints if you rely on preflight, and verify Origin server side.
  • Keep credentialed CORS allowlists exact: no reflection, no null, no broad regexes, plus Vary: Origin.
  • Set SameSite explicitly on every session cookie and use the __Host- prefix where possible.
  • Remove every state-changing GET handler and protect login forms.
  • Replace archived CSRF middleware with a maintained option or a Fetch Metadata check.

Standards and references

Related NHI Mgmt Group resources: CSRF Prevention Guide · CORS Misconfiguration Guide · CSP vs CORS · XSS vs CSRF · Fetch Metadata and Resource Isolation Policy Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org