Skip to content
security

Same-Origin Policy, CORS, and CSRF: What Actually Protects Your Login Flows

Updated · 11 min read · By

On this page

Key takeaways

  • An origin is scheme + host + port. Two URLs share an origin only when all three match exactly.
  • The same-origin policy stops scripts from reading cross-origin responses. It does not stop the browser from sending cross-origin requests.
  • That gap is where CSRF lives: a forged request rides the victim's cookies even though the attacker never sees the response.
  • CORS is an opt-in relaxation of the same-origin policy set by the server. It is never a CSRF defense on its own.
  • SameSite=Lax cookies plus a CSRF token or an Origin / Sec-Fetch-Site check close CSRF for login, signup, and account settings.

What is the same-origin policy?

SOP was never designed to stop requests. It controls what a page can read. Browsers still let a page embed images, scripts, and stylesheets from anywhere, submit forms to anywhere, and navigate anywhere. That distinction between sending and reading is the source of most confusion about CSRF and CORS, so it is worth getting precise.

What counts as the same origin?

An origin is the triple of scheme, host, and port, as defined in RFC 6454. Two URLs have the same origin only when all three match. Paths, query strings, and fragments do not matter. When the port is omitted, the scheme's default port applies (80 for HTTP, 443 for HTTPS).

Compared against https://example.com/account:

URLSame origin?Why
https://example.com/settings?tab=mfaYesOnly the path and query differ
https://example.com:443/loginYes443 is the default HTTPS port
http://example.com/accountNoDifferent scheme
https://app.example.com/accountNoDifferent host
https://example.com:8443/accountNoDifferent port
https://example.co/accountNoDifferent host

A different port is a different origin. That trips teams running an auth server on :8443 next to the app on :443: the browser treats them as strangers, so every call between them needs CORS.

What the same-origin policy allows and blocks

In practice for a CIAM deployment:

  • Allowed: a page on evil.example can submit a hidden form that POSTs to https://bank.example/transfer. The browser sends it.
  • Allowed: that page can load <img src="https://bank.example/logout">. The browser sends that GET too.
  • Blocked: a script on evil.example calling fetch("https://bank.example/api/me") cannot read the JSON that comes back.
  • Blocked: the page cannot read the DOM of an iframe showing bank.example.

The old escape hatch document.domain, which let two subdomains relax SOP between themselves, is deprecated. Chromium stopped honouring the setter by default in version 115, so do not design new flows around it.

Why the same-origin policy does not prevent CSRF

A typical CSRF against a customer account looks like this:

  1. The victim is signed in to shop.example, with a session cookie in the browser.
  2. The victim opens a page on evil.example.
  3. That page auto-submits a hidden form to https://shop.example/account/email with the attacker's address.
  4. If the session cookie rides along and the server trusts it alone, the email changes. A password reset to the new address completes the account takeover.

The same pattern hits every sensitive identity endpoint: email and phone change, MFA enrolment and removal, connected-app consent, password change without re-authentication, and logout.

Login CSRF is the inverted version. The attacker forges a login request with their own credentials, signing the victim into the attacker's account. Anything the victim saves afterwards, including payment details, lands in an account the attacker controls. Login forms need CSRF protection too.

Where CORS fits (and where it does not)

The rules that matter for identity APIs:

  • Simple requests are sent without asking. A GET, or a POST with a form-style content type, goes straight to the server. CORS only decides whether the calling script may read the answer.
  • Other requests trigger a preflight. A JSON POST, a custom header, or methods like PUT and DELETE make the browser send an OPTIONS request first. The real request is sent only if the server approves.
  • Credentials need an exact origin. To allow cookies, the server must return Access-Control-Allow-Credentials: true and name the exact origin in Access-Control-Allow-Origin. The wildcard * is rejected for credentialed requests.
  • Never reflect the Origin header blindly. Echoing any incoming origin back with credentials allowed hands every site on the internet read access to your authenticated API.

JSONP, the pre-CORS trick of wrapping data in a <script> callback, bypasses SOP entirely by design. Retire any JSONP endpoint that returns user data.

How to prevent CSRF in customer login and account flows

The OWASP CSRF Prevention Cheat Sheet is the reference. Applied to CIAM:

  1. SameSite cookies. SameSite=Lax blocks the session cookie on cross-site POSTs and subresource loads while keeping top-level navigation working. Use Strict for admin consoles. Chromium has treated cookies without the attribute as Lax by default since 2020, but set it explicitly: other browsers and embedded webviews differ.
  2. Synchronizer or signed double-submit token. Put a per-session token in each form and verify it server-side. Most frameworks ship this. Sign double-submit tokens and bind them to the session, or a subdomain that can set cookies can forge them.
  3. Origin and Fetch Metadata checks. Reject state-changing requests whose Origin header is not yours. Where supported, reject requests with Sec-Fetch-Site: cross-site on sensitive endpoints. Both are cheap and need no client changes.
  4. Re-authentication for high-risk actions. Require a fresh password, passkey, or MFA check before email change, MFA removal, and payout changes. This also caps the damage from a stolen session. See token lifetime best practices.
  5. Protect the login and signup forms. Login CSRF and forced signup are real. Apply the same token or Origin check to unauthenticated forms.

SameSite works on the site boundary, not the origin boundary. app.example.com and blog.example.com are same-site, so a compromised or user-controlled subdomain can still send same-site requests. Keep user-generated content on a separate registrable domain.

Token-based APIs and OAuth redirects

Single-page apps that send an access token in an Authorization header, instead of relying on cookies, are not exposed to classic CSRF: the browser never attaches that header automatically. The trade-off is that a token readable by JavaScript is exposed to XSS. The session management guide covers why HttpOnly cookies remain the default for browser sessions.

OAuth and OIDC redirect flows have their own CSRF surface: an attacker can inject their own authorization code into the victim's callback. The defenses are the state parameter, bound to the user's session, and PKCE, which OAuth 2.1 makes mandatory for public clients. The OAuth 2.1 guide walks through both.

A quick CSRF checklist for CIAM teams

  • Session cookies are Secure, HttpOnly, and SameSite=Lax or stricter.
  • Every state-changing endpoint, including login, signup, and logout, checks a CSRF token or the Origin header.
  • No GET request changes state.
  • CORS allowlists exact origins; no reflected origins with credentials; no JSONP.
  • Email, phone, password, MFA, and payout changes require recent re-authentication.
  • OAuth callbacks validate state and use PKCE.
  • User-generated content lives on a separate registrable domain.

Related guides

Where to next

FAQ

What is the same-origin policy?
The same-origin policy is a browser rule that stops a script loaded from one origin from reading data returned by another origin. An origin is the scheme, host, and port of a URL. The policy keeps a malicious page from reading your bank session, inbox, or account settings through your logged-in browser.
What counts as the same origin?
Two URLs share an origin only when scheme, host, and port all match. https://example.com and https://example.com:443 are the same origin because 443 is the default HTTPS port. http://example.com, https://app.example.com, and https://example.com:8443 are each a different origin from https://example.com.
Does the same-origin policy prevent CSRF?
No. The same-origin policy blocks reading cross-origin responses, but browsers still send cross-origin form posts, image loads, and navigations, and they attach cookies to them. A CSRF attack only needs the request to arrive, not the response. Defend with SameSite cookies plus a CSRF token or Origin header check.
Does CORS protect against CSRF?
No. CORS tells the browser which other origins may read a response. A cross-origin form post needs no CORS permission at all, so a missing CORS header does not stop it from reaching your server. A misconfigured CORS policy can make CSRF worse by letting the attacker read the response too.
What is the difference between same-origin and same-site?
Same-origin compares scheme, host, and port exactly. Same-site compares the scheme and the registrable domain, so https://app.example.com and https://login.example.com are same-site but cross-origin. SameSite cookies use the site boundary, which means a compromised sibling subdomain can still send same-site requests.
What is login CSRF?
Login CSRF forges a login request that signs the victim into the attacker's account. Anything the victim then saves, such as a card number or search history, lands in an account the attacker controls. Protect the login form with a CSRF token or Origin check, just like any other state-changing form.

Sources

Last reviewed 2026-09-28.