Cross-Origin Requests and CORS: A 2026 Review and Update

Security · Web Platform · 2026

Cross-Origin Requests and CORS: A 2026 Review and Update

Same-Origin Policy has existed since 1995, and the CORS spec was set over a decade ago. Revisiting it in 2026, the rules have not changed one bit; people who understand architecture already barely notice it is there.

~5,500 wordsCORSSecurityArchitectureWeb Platform

Why Is Cross-Origin a Problem?

Before we get into CORS, step back and ask a more fundamental question: why is cross-origin a problem at all? The answer shapes how deeply you understand the whole mechanism.

The root of cross-origin issues is a behavior browsers have that other tools do not: they automatically attach cookies.

Picture this: you log into facebook.com, and the browser stores a session cookie for you. Then you open another tab to a site you do not recognize — evil.com. That page runs JavaScript that quietly sends a request to facebook.com/api/messages.

Here is the problem: the browser will attach your Facebook cookie automatically. Without any restrictions, JavaScript on evil.com could read your private messages or even delete your account — all without ever knowing your password.

The Core Issue
Because browsers attach cookies automatically, any site can potentially impersonate your logged-in identity and send authorized requests to other sites on your behalf.

That is the prototype of a CSRF (Cross-Site Request Forgery) attack. To defend against it, browsers ship with the Same-Origin Policy built in.

Notice we said "browser." curl, Postman, Node.js, your backend services — these tools do not attach cookies automatically, have no concept of a logged-in session, and never face the scenario of "a user is browsing another site at the same time." They never needed Same-Origin Policy in the first place. Cross-origin problems have always been a browser-only concern.

Key Insight
"Why does Postman work but the frontend hits a CORS error?" — because Postman has no browser referee. The problem is not the API; it is the backend not setting the right response headers.

Same-Origin Policy: The Browser's Referee Rules

Same-Origin Policy is a security rule baked into every browser (Chrome, Firefox, Safari) out of the box. Nobody configures it, and nobody can turn it off. The rule is simple: JavaScript can only read same-origin responses.

"Same origin" means all three of these must match:

ConditionExample
Protocolhttps:// vs http:// → different origin
Domainapp.example.com vs api.example.com → different origin
Portlocalhost:3000 vs localhost:4000 → different origin

If any one of the three differs, it is cross-origin, and the browser blocks JavaScript from reading the response.

Origin Header: The Foundation of the Whole Mechanism

How does the browser know where a request came from? Through the Origin header on the request. The browser sets it automatically, and it is locked — JavaScript cannot override it.

You might think: "What if I add Origin: app.example.com to the request myself?" It will not work. The browser quietly ignores your value and replaces it with the real origin. No error — just a silent override.

Why Silent Override?
If it threw an error, malicious code could catch it and work around the check. Silent ignoring lets attackers think they succeeded when they did not — that is deliberate design.

The foundation of the entire CORS trust model is this: the backend trusts the Origin the browser sends, and the browser guarantees that value is real and cannot be forged.

CORS Mechanism Breakdown

Same-Origin Policy was designed for security, but it also blocks legitimate use cases. Your own frontend at app.example.com wants to call your own backend at api.example.com — the browser does not know that is "your team," so it blocks anyway.

CORS (Cross-Origin Resource Sharing) is the mechanism that fixes this. It lets the backend tell the browser: "I allow this origin — you can let it through."

Same-Origin Policy is the lock; CORS is the key the backend hands to the browser.

Who Sets What?

MechanismSet ByWhere
Same-Origin PolicyBuilt into the browser; no one configures it
Access-Control-Allow-OriginBackend developerResponse header
CSRF TokenGenerated by backend, placed in form by frontendRequest body / header

One detail that matters a lot: Access-Control-Allow-Origin only means anything when it is on the response. The browser checks this header after receiving the response, then decides whether to expose the body to JavaScript.

Some people ask: "If I add this header on the request, will my call go through?" No. The backend sees it and does nothing, because this header is not for the backend. The referee is the browser, and it only looks at this header when the response arrives.

The Boundary of CORS Protection

Here is a detail many people miss:

Important Boundary
The browser has already sent the request, and the backend has already executed it — CORS blocks whether JavaScript can read the response, not whether the request can be sent.

That distinction matters:

Operation TypeProtected by CORS?Why
GET to read personal data✅ YesJS cannot read the response; data stays put
POST/DELETE to trigger actions⚠ Not necessarilyRequest is sent and backend runs before CORS is checked

That is why CORS alone is not enough — requests that trigger side effects need additional protection. Which brings us to Preflight and CSRF tokens.

Preflight: Ask Before You Send

Since CORS cannot stop a request from going out, the browser added another mechanism: for potentially dangerous requests, before sending the real one, it sends a scout request to ask the backend: "I am about to hit you like this — is that okay?"

That scout request is Preflight, using the OPTIONS method.

BrowserServerOPTIONS /api/dataOrigin: app.example.com | Access-Control-Request-Method: DELETE200 OK — 允許Access-Control-Allow-Origin: app.example.com | Allow-Methods: DELETE✓ Preflight 通過,送出真正的請求DELETE /api/data200 OK — 資料回傳Access-Control-Allow-Origin: app.example.com✓ 瀏覽器確認,JavaScript 可以讀取

Which Requests Trigger Preflight?

The browser splits requests into two buckets: simple requests and non-simple requests. The test is one principle:

Could an HTML <form> or <a> tag have sent this request natively?
Yes → no Preflight. No → ask first.

Concretely, a request is "simple" only when all of the following hold:

ConditionAllowed Values
MethodGET / POST / HEAD
Content-Typeapplication/x-www-form-urlencoded / multipart/form-data / text/plain
HeadersOnly browser-native headers; no custom headers

Fail any one condition and Preflight fires. In modern development, that is almost guaranteed:

  • Nearly every API uses application/json (not on the allowed list)
  • JWT auth needs Authorization: Bearer xxx (custom header)
  • REST APIs commonly use PUT / DELETE (not allowed methods)
Practical Note
Decoupled frontends and backends almost always trigger Preflight. That is not a bug — it is normal. The real problem is not knowing what it does and treating it like one.

Preflight Performance

Every non-simple request sends an OPTIONS first, waits for the reply, then sends the real request. For high-frequency APIs, that extra round-trip has a cost.

The fix is for the backend to add Access-Control-Max-Age on the Preflight response, telling the browser how long to cache the result. During that window, identical requests to the same endpoint skip Preflight.

# Tell the browser to cache this Preflight result for 1 hour
Access-Control-Max-Age: 3600

CSRF Token: Last Line of Defense for Simple Requests

Preflight handles non-simple requests by asking before sending. Simple requests never trigger Preflight — they go out, and the backend runs.

There is a subtle point: even for simple requests, JavaScript on evil.com cannot read the response (thanks to CORS). But if the request has side effects — say, a POST form submission — the backend already executed it, whether or not JavaScript can see the response.

For state-changing simple requests, the backend needs another check. That is the CSRF token.

How It Works

01
Backend generates a token

When the user loads the page, the backend generates a random token unique to that session and embeds it in the HTML form

02
Frontend submits with the token

When the user submits the form, the token travels with the request to the backend

03
Backend verifies the token

The backend checks whether the token matches; if not, it rejects the request

Why can't evil.com get that token? Because it lives in HTML served from app.example.com, and JavaScript on evil.com trying to read a page from app.example.com is a cross-origin request — blocked by Same-Origin Policy.

CSRF token protection rides on top of Same-Origin Policy.
Same-Origin Policy keeps the token from being stolen; the token keeps simple requests from being forged.

You may have seen this in Laravel or similar frameworks:

<!-- Laravel Blade form -->
<form method="POST" action="/profile">
    @csrf
    <!-- Expands to: -->
    <!-- <input type="hidden" name="_token" value="abc123xyz..."> -->
    ...
</form>

The Full Protection Chain

Put all of these mechanisms together and you can see how they prop each other up:

Same-Origin Policy瀏覽器內建 · 無法關閉 · 保護「JS 讀不到跨域 response」→ evil.com 讀不到你的頁面 HTML、讀不到你的 API response、偷不到 CSRF tokenCORS Header後端設定 · Access-Control-Allow-Origin · 合法跨域的白名單→ 告訴瀏覽器哪些 origin 可以讀 response,其他一律擋Preflight(OPTIONS)瀏覽器自動觸發 · 非簡單請求才有 · 在送出前先確認後端允許→ DELETE / PUT / 自訂 header / application/json → 先問,不讓危險請求直接送出CSRF Token開發者實作 · 彌補簡單請求沒有 Preflight 的空缺→ 後端驗 token,拿不到 token 的請求(evil.com)一律拒絕

These four layers are not independent — they interlock. Remove any one and the whole system springs a leak. Knowing what each layer does tells you where to look when something breaks.

2026 Architecture Thinking: Make the Problem Vanish

Back to the opening question: in 2026, is there anything new about cross-origin?

Not in the mechanisms. Same-Origin Policy, CORS headers, Preflight — all settled a decade ago, and the spec has not moved.

What changed is this: modern architecture has turned "needing to solve cross-origin in the browser" into a design smell.

How We Used to Solve It

Five years ago, a typical fix looked like this:

// NestJS: enable CORS so the frontend can call the API
app.enableCors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
});

Nothing wrong with that — it is still correct today if your architecture needs the frontend to call the backend directly. But most of the time, that "if" can be designed away.

Server Components: Cross-Origin Disappears on the Server

As we covered earlier, cross-origin only exists in the browser environment because the browser is the referee. What if the request is never sent from the browser at all?

That is the idea behind Next.js Server Components. API calls run on the server; the browser only renders the result. Server to server — no browser, no referee, no cross-origin problem.

// Server Component — runs on the server, not in the browser
async function UserProfile() {
  // Call an external API directly — no CORS issue
  // because this does not run in the browser
  const data = await fetch('https://api.external-service.com/user');
  const user = await data.json();

  return <div>{user.name}</div>;
}

Edge Middleware: The Browser Thinks It Is Same-Origin

Another broadly applicable pattern is Edge Middleware (Cloudflare Workers, Next.js Middleware, Vercel Edge Functions).

The idea is simple: put a proxy between the browser and the backend. The browser calls app.example.com/api/xxx; the middleware forwards to api.external.com/xxx and returns the response. The browser never leaves same-origin, so CORS never enters the picture.

(瀏覽器認為同源)app.example.com/api/xxx(Middleware 代理)api.external.com/xxxBrowserEdge MiddlewareExternal API✓ 瀏覽器以為同源,無 CORS 問題

BFF (Backend for Frontend)

A more traditional but equally solid approach is the BFF (Backend for Frontend) pattern: the frontend only talks to its own backend (same origin), and that backend calls everything else. The browser always stays same-origin; cross-origin work happens server-to-server, never in front of the browser referee.

What's New in 2026: Private Network Access

One spec that landed in recent years deserves attention: Private Network Access (PNA).

Chrome has been rolling out enforcement since 2022: a public page (https://app.example.com) trying to reach a private network (localhost, 192.168.x.x, internal IPs) needs an extra response header from the target:

Access-Control-Allow-Private-Network: true

That hits homelab-plus-cloud-frontend setups hard. If your local localhost:3000 must be reachable from an HTTPS page, you need to handle this header explicitly.

New Cross-Origin Scenarios from AI Agents

In 2026, the interesting shift is not the spec — it is what AI agent architectures add to the picture.

Classic cross-origin is frontend → backend. Agent setups add backend services dynamically calling tools and third-party APIs, while OAuth callbacks, webhooks, SSE streams, and MCP server endpoints all need deliberate cross-origin strategy.

The rules are the same; the surface area is much larger. An agent might talk to dozens of origins, each with its own CORS config — miss one and a specific tool invocation fails in production.

Note for n8n / Agent Developers
n8n workflows and backend-to-backend HTTP Request nodes run outside the browser — no CORS. But if the n8n UI itself calls an API, you are back under browser rules. The key question is always: who is sending this request?

Conclusion

Looking back at cross-origin in 2026, the takeaway is not some shiny new spec — it is how many of us, myself included, never really understood what this stack was protecting in the first place.

Core Ideas at a Glance

  • Cross-origin is a browser rule— curl, Postman, and backend services are unaffected
  • Same-Origin Policy is the lock— it stops JS from reading cross-origin responses
  • CORS headers are the key— set on the response, they tell the browser which origins are allowed
  • Preflight is the scout— non-simple requests ask the backend before sending, so dangerous operations do not run blindly
  • CSRF tokens patch the simple-request gap— Same-Origin Policy keeps the token from being stolen
  • Modern architecture makes the problem vanish: Server Components, Edge Middleware, and BFF keep cross-origin off the browser layer

Not a line of the rules has changed. People who understand architecture already barely notice it is there.

If you are designing a new system and find yourself researching CORS headers, pause and ask: can this cross-origin call disappear at the architecture layer? Often, it can.


Last updated on 2026-06-10. If you spot an error or have something to add, feel free to email me to discuss.