Daniel sent us this one — and it's a topic that's tripped up basically every web developer at some point. He wants a dummy's guide to CORS, Cross-Origin Resource Sharing. What it actually is, why browsers enforce it, why developers keep stumbling over it, and the most common pitfalls with how to avoid them. He's pitching this at someone who's stared at that red console error — you know the one — and has no idea what's going on or why slapping a wildcard on everything is a bad idea.
Oh, this is such a good one. And I love that he specifically called out the wildcard thing, because that is the universal first instinct. You see the error, you Google it, the first Stack Overflow answer says "just add Access-Control-Allow-Origin star," and suddenly you've papered over the problem without understanding any of it.
Guilty as charged. I think I did exactly that on my first project. And by the way, today's episode is powered by DeepSeek V four Pro.
Alright, so let's start with the thing that surprises most people when they actually dig into it. CORS is not a security feature. It's a relaxation of security. The browser's default posture is called the Same-Origin Policy, and it is aggressively restrictive. If JavaScript from one origin tries to read a response from a different origin, the browser says no. That's the default.
The Same-Origin Policy is the actual security mechanism, and CORS is the browser saying "alright server, if you explicitly tell me this other origin is allowed, I'll make an exception.
And that distinction matters because it completely reframes what CORS is doing. A lot of developers think CORS protects their server from unauthorized access. It doesn't. An attacker can fire up curl or Postman or write a server-side script and hit your API directly with no CORS restrictions whatsoever. CORS protects the user's browser from being tricked into reading sensitive data from another site. The victim is the end user, not your server.
That's a subtle point that I think most introductory explanations miss. The threat model here is not "someone hacks my API." The threat model is "a malicious website tricks a logged-in user's browser into pulling data from their bank, and then exfiltrates it.
And that's why the Same-Origin Policy exists in the first place. Imagine you're logged into your bank at bank dot com. You have a session cookie. Then you open a new tab and visit evil dot com. Without the Same-Origin Policy, evil dot com's JavaScript could make a fetch request to bank dot com slash account slash balance, and the browser would happily attach your session cookie and send back your account balance. The JavaScript on evil dot com reads the response, ships it off to the attacker, and you never know it happened.
The browser's default rule is: if the JavaScript making the request came from origin A, it can only read responses from origin A. And an origin, by the way, is not just the domain name. It's the full tuple — scheme, host, and port.
This is where developers get bitten constantly during local development. Your React dev server is running on localhost port three thousand. Your Express API is on localhost port five thousand. Those are different origins. The scheme is the same, the host is the same, but the port is different, so the browser treats them as completely separate. That's why you hit CORS errors in development even though everything is on localhost.
I've seen so many people confused by that. They're like "but it's all localhost, why is this happening?" And the answer is that port number. Port three thousand and port five thousand are as different as example dot com and totally different site dot com as far as the browser is concerned.
HTTPS versus HTTP matters too. If your frontend is served over HTTPS and your API is on HTTP, those are different origins. The scheme is part of the origin definition. So let's walk through what actually happens mechanically when a cross-origin request is made, because understanding the flow is what makes the error messages make sense.
Yeah, walk us through it. I'm making a fetch call from my frontend to an API on a different origin. What does the browser actually do?
So you write fetch, you pass the URL of the API. The browser looks at the origin of the page you're on and the origin of the API you're calling. If they match, it just sends the request normally and lets you read the response. If they don't match, the browser adds an Origin header to the request. Something like Origin colon https colon slash slash myapp dot com. That header tells the server "hey, this request is coming from a page on this origin.
The server sees the Origin header and needs to make a decision.
The server's response needs to include an Access-Control-Allow-Origin header. If the value of that header matches the origin that made the request, or if it's a wildcard star for public data, the browser allows the JavaScript to read the response. If that header is missing, or if the value doesn't match, the browser blocks the read. The request still reaches the server. The server still processes it. The response comes back. But the browser refuses to hand that response to your JavaScript code.
That last part is critical. The request went through. The server did its thing. If it was a POST request, the data was written. The block happens entirely on the read side, inside the browser. Which is why if you look at the network tab, you'll actually see the response there with all the data. It's just that your JavaScript can't access it.
That's one of the most confusing parts for people debugging this. They open the network tab, they see a two hundred OK response with all the JSON they expected, and they think "the request worked, so what's the problem?" The problem is that the browser is protecting the user by not letting that rogue JavaScript read the response.
Alright, so that's the basic flow. But then there's this whole preflight thing. What's the deal with OPTIONS requests?
The browser divides cross-origin requests into two categories. Simple requests and everything else. A simple request is basically a GET or a POST with standard headers and a Content-Type of application slash x-www-form-urlencoded, multipart slash form-data, or text slash plain. That's it. If your request meets those criteria, the browser sends it directly and then checks the response headers.
How many real-world API calls actually qualify as simple?
As soon as you set Content-Type to application slash JSON — which is basically every modern API — you're out of simple territory. As soon as you add an Authorization header with a bearer token, you're out. If you use PUT or DELETE or PATCH, you're out. At that point the browser says "hold on, this request could have side effects, I need to ask permission first.
That's the preflight.
That's the preflight. The browser sends an OPTIONS request to the same URL before sending the real request. It includes headers like Access-Control-Request-Method to say "I want to send a PUT request" and Access-Control-Request-Headers to say "I want to include an Authorization header." The server then needs to respond with the appropriate Access-Control-Allow-Methods and Access-Control-Allow-Headers headers. If the server says yes, the browser sends the real request. If the server doesn't handle the OPTIONS request properly, the real request never happens.
This is another huge source of confusion, because developers will see an OPTIONS request in their network tab, see that it returned a four hundred or a five hundred error, and not understand why their actual POST or PUT never fired.
The preflight failed, so the browser aborted the whole thing. And a lot of backend frameworks don't handle OPTIONS requests by default. You have to explicitly configure your CORS middleware to respond to them. If you're using Express with the cors package, it handles this automatically. But if you're rolling your own solution or using something more bare-bones, you might miss it.
Let's talk about the wildcard problem, because Daniel specifically called that out. Why is Access-Control-Allow-Origin star a bad idea?
There are really three reasons. The first is the most obvious: it means any website on the internet can read your API's responses. If your API returns public data — like weather information or a public feed — that's fine. But if it returns anything user-specific, anything behind authentication, you've just told the browser "sure, let any random website read my users' data.
The second reason?
The second reason is that the wildcard is incompatible with credentialed requests. If your frontend sends cookies or HTTP authentication — which in fetch means setting credentials to include, or in the older XMLHttpRequest API setting withCredentials to true — the server cannot respond with a wildcard. It must return the exact origin. The browser will reject any credentialed response that uses a wildcard. This is a hard spec requirement, not a suggestion.
If you build your app with a wildcard during development and then later add authentication, suddenly everything breaks and you have to reconfigure.
And that's the third reason. Using a wildcard is a shortcut that teaches you nothing and will break the moment your app gets more sophisticated. You're better off configuring CORS properly from the start, even in development, so you understand what's happening and your setup works when you add auth.
Let's talk about some of the more dangerous misconfigurations, because there are patterns that look like they work but are actually security holes. I've seen code that just echoes back whatever Origin header the client sent.
This is a classic. The server takes the Origin header from the request and copies it verbatim into the Access-Control-Allow-Origin response header. It looks like it's working — any origin that makes a request gets a matching header back. But this means a malicious site can make credentialed requests to your API, and the browser will happily return the response because the server said "yeah, that origin is fine.
It completely defeats the purpose. You've effectively implemented a wildcard but in a way that also works with credentials, which is even worse.
There's a great example of this from real-world security research. The PortSwigger Web Security Academy has a whole lab on exploiting CORS misconfigurations. If a site reflects the Origin header and also supports credentialed requests, an attacker can craft a page that makes authenticated requests to the vulnerable API, reads the response, and exfiltrates user data. And the user just sees a blank page or a broken image — they have no idea their data was just stolen.
There's another nasty one involving null origins. What's that about?
Certain contexts cause the browser to send Origin colon null. Sandboxed iframes are the big one. If you use an iframe with the sandbox attribute, and you don't include allow-same-origin, the browser sends Origin colon null for any requests from that iframe. Some developers see CORS errors from these contexts and think "oh, I'll just whitelist null as an allowed origin." But an attacker can craft a sandboxed iframe that makes requests to your API, and because you've whitelisted null, the browser allows it. There's a writeup from Outpost24 a couple years back that walks through exactly how this exploit works in practice.
Null in the allowed origins list is basically a trap.
It's a trap, yeah. Don't do it. Alright, so we've covered what CORS is, why the Same-Origin Policy exists, the mechanics of simple requests and preflights, and the common misconfigurations. Let's talk about how to actually fix CORS errors properly, because that's what Daniel's listener really needs.
So you're developing locally, your React app on port three thousand is trying to talk to your API on port five thousand, and you get the CORS error. What's the right fix?
The correct fix is to configure your backend to return the proper CORS headers. If you're using Express, you install the cors npm package and configure it with the specific origin you want to allow. Something like cors open paren open brace origin colon quote https colon slash slash localhost colon three thousand quote close brace close paren. In Flask, you use flask-cors. In ASP dot NET, there's built-in CORS middleware. Every major framework has a well-tested CORS library.
Don't roll your own.
Please don't roll your own. These libraries handle edge cases, they handle preflight properly, they set the right headers in the right order. It's not worth the risk of getting it wrong.
What about the proxy approach? I know Create React App has a built-in proxy feature.
Yeah, a development proxy is a perfectly valid approach. The idea is that instead of your frontend making requests directly to the API on a different port, you configure your dev server to proxy certain requests to the API. So your frontend thinks it's talking to the same origin — it makes a request to slash api slash whatever on port three thousand, and the dev server forwards that request to port five thousand behind the scenes. From the browser's perspective, it's a same-origin request, so CORS never enters the picture.
That's actually cleaner in some ways because your development setup more closely mirrors production, where you'd typically have a reverse proxy like nginx handling this.
In production, you'd typically serve both your frontend and your API from the same domain, using nginx or a similar reverse proxy to route requests. So slash goes to your static files, and slash api goes to your backend. Same origin, no CORS issues. Setting up a proxy in development mimics that architecture.
What about the browser extension approach? I see a lot of people recommending "just install this CORS Unblock extension.
This is a terrible habit. Those extensions work by stripping CORS headers or adding wildcard headers to every response. They completely disable the browser's security model for every website you visit, not just your development server. And they mask the problem — your app works during development with the extension enabled, and then you deploy it and it breaks for every real user. You've learned nothing and you've built a broken app.
It's the equivalent of testing your car's brakes by disabling them and saying "well, it drives fine.
That's a perfect analogy. And there's an infamous real-world example of what happens when developers hack around CORS instead of learning it. In twenty nineteen, Zoom had a vulnerability — CVE twenty nineteen dash one three four five zero — where their developers couldn't get CORS working for their localhost web server. So instead of fixing the CORS configuration, they encoded server responses in image dimensions loaded through image tags. Image tags don't have CORS restrictions. You can load an image from any origin.
Wait, they encoded data in image dimensions? Like, the width and height of an image?
They would generate a tiny image where the width and height encoded the data they wanted to send back to the client. The JavaScript would load the image, read the dimensions, and decode the data. It completely bypassed CORS, but it also meant that any website could load that image and extract the data. Chris Foster wrote a great breakdown of this at the time, pointing out that this was a direct result of developers not understanding CORS and reaching for a hack instead.
That is an incredible cautionary tale. "We couldn't figure out CORS, so we invented a data encoding scheme using image dimensions." And it left a localhost web server accessible to any website.
This is the thing — CORS errors are frustrating, but they're signaling a real security boundary. When you hit a CORS error, the browser is doing its job. Your job as a developer is to configure your server to explicitly say which origins are allowed, not to find a way around the browser's protections.
Let's talk about the preflight performance issue. You mentioned that non-simple requests trigger an extra round trip. On high-latency connections or APIs with many endpoints, that adds up.
Every preflight is a full HTTP round trip. If you're on a mobile connection with three hundred milliseconds of latency, that's an extra three hundred milliseconds before your actual request even starts. And if you have multiple API endpoints, each one might trigger its own preflight.
Is there a way to mitigate that?
Yes, there's the Access-Control-Max-Age header. This tells the browser "you can cache the preflight response for this many seconds." The maximum value browsers respect is eighty six thousand four hundred seconds, which is twenty four hours. So if you set Access-Control-Max-Age colon eighty six thousand four hundred, the browser will do the preflight once and then reuse the cached result for all subsequent requests to that origin for the next day.
Most developers don't set this.
Most developers don't even know it exists. But it's a one-line addition to your CORS configuration that can meaningfully improve perceived performance for your users on slow connections. In Express with the cors package, it's just the maxAge option.
We've covered the mechanics, the pitfalls, the fixes. Let's zoom out for a second. You mentioned earlier that CORS only applies to browser-based requests. I think that's worth emphasizing because it explains so much confusion.
It really does. If you test your API with curl or Postman or write a Python script to hit it, CORS does not apply. Those tools are not browsers, they don't enforce the Same-Origin Policy, they don't send preflight requests, they don't care about Access-Control-Allow-Origin headers. So if your API works perfectly in Postman but fails in the browser, CORS is almost certainly the culprit.
That's the first diagnostic step. If it works in Postman and not in the browser, you're dealing with CORS. If it doesn't work in Postman either, you have a different problem.
And I think that leads to a broader point about how CORS should be taught. Most tutorials present it as a nuisance, a hurdle to get past. "Here's how to disable CORS so you can get on with building your app." But that framing misses the entire point. CORS is a cooperative mechanism between browsers and servers to protect users. Understanding why it exists and how it works makes you a better developer.
There's also an interesting question about whether the preflight mechanism is still the right design. It was created in an era when cross-origin requests were relatively rare and most APIs were simple. Now we have single-page applications making dozens of cross-origin API calls, microservices architectures, and GraphQL endpoints that bundle many operations into a single request. The preflight overhead is more noticeable than it used to be.
It's a fair question. The preflight mechanism adds complexity and latency. But the security model it enforces is still valuable. Without preflight, a malicious site could send PUT or DELETE requests that modify data on another origin, and the browser would have no way to ask permission first. The preflight is essentially the browser saying "server, are you sure you want to accept this kind of request from this origin?
The server gets to make a granular decision. It can say "yes to GET requests from this origin, but no to DELETE requests.
The preflight response can include Access-Control-Allow-Methods to specify exactly which HTTP methods are allowed. It can include Access-Control-Allow-Headers to specify which custom headers are acceptable. It's a fine-grained permissions system, not a binary on-off switch.
Alright, let's synthesize this into practical takeaways. If you're the developer Daniel is describing — you've hit a CORS error, you're frustrated, you don't know what's going on — what's your step-by-step plan?
Step one: understand that this is the browser protecting your users, not a bug. Step two: check if your request is cross-origin. Are the scheme, host, and port all the same between your frontend and your API? If the port is different, that's cross-origin. Step three: figure out if your request is simple or if it triggers a preflight. If you're sending JSON or using custom headers, you need to handle OPTIONS requests on your server.
Step four: configure your backend properly. Use a well-tested CORS library for your framework. Specify the exact origin you want to allow, not a wildcard, unless your API is truly public data. Step five: if you're in development, consider using a proxy instead of dealing with CORS directly. It more closely mirrors production architecture.
Step six: never install a browser extension that disables CORS. It's a security risk and it masks the problem. If you absolutely must test something quickly and you understand the risks, you can launch Chrome with web security disabled using a command line flag, but close that browser instance as soon as you're done testing.
One more thing I want to touch on. The research from PortSwigger highlights that CORS misconfigurations are consistently found in real-world security assessments. This isn't a theoretical problem. Sites get hacked because of overly permissive CORS policies.
The attacks are often subtle. An attacker finds a site that reflects the Origin header and supports credentialed requests. They craft a malicious page that makes an authenticated request to the vulnerable API. The user visits the page, their browser sends their session cookie, the server responds with the user's data and an Access-Control-Allow-Origin header that matches the attacker's origin. The attacker's JavaScript reads the response and sends it to a server they control.
From the user's perspective, nothing unusual happened. The page might have looked broken or blank, but there's no alert, no warning. Their data was exfiltrated silently.
That's why proper CORS configuration matters. It's not about getting your app to work. It's about making sure your app doesn't become a vector for stealing your users' data.
One thing we haven't talked about is how CORS interacts with content delivery networks and third-party APIs. If you're loading resources from a CDN, those resources typically don't need CORS because they're loaded via script tags or link tags, not via fetch. But if you're making API calls to a third-party service, that service needs to include your origin in their Access-Control-Allow-Origin header.
That's why when you're integrating with a third-party API, you often need to register your domain with them. They add your origin to their allowlist, and then their responses include the right header. If they didn't do this, any website could make authenticated requests to their API on behalf of their users.
Which circles back to the point about CORS protecting users, not servers. The third-party API is protecting its users from having their data accessed by malicious sites.
And now: Hilbert's daily fun fact.
The collective noun for a group of porcupines is a prickle.
So if you're the developer staring at that red console error, here's what you actually do. First, verify it's a CORS issue by testing the same request in curl or Postman. If it works there and not in the browser, you've confirmed it. Second, configure your backend CORS properly using a library, not a hand-rolled solution. Third, be specific about which origins you allow. Fourth, set Access-Control-Max-Age to reduce preflight overhead. Fifth, never use a browser extension to disable CORS.
If you're building a new project, set up CORS correctly from day one in development. Configure your backend to accept requests from your frontend's development origin. Don't use a wildcard as a placeholder. The configuration you write during development should be as close as possible to what you'll deploy. That way you're not scrambling to fix CORS issues the night before launch.
The broader lesson here is that CORS errors are not random annoyances. They're the browser enforcing a security boundary that exists for good reason. Learning how to work with that boundary instead of hacking around it makes you a more competent developer and makes your applications safer for your users.
Honestly, once you understand the Same-Origin Policy and the purpose of the preflight mechanism, CORS stops being this mysterious source of frustration and becomes just another piece of web infrastructure that you know how to configure. It's not that complicated. It's just that most people's first exposure to it is a cryptic error message and a Stack Overflow answer that says "add star.
Which teaches you nothing and sets you up for problems down the road. The fifteen minutes it takes to actually read the MDN docs on CORS is time well spent. The MDN Web Docs entry on this is excellent — it's the definitive reference and it's kept up to date. Last updated just a couple of days ago, actually.
If you want the security perspective, the PortSwigger Web Security Academy has a whole module on CORS with hands-on labs where you can practice exploiting misconfigurations. There's nothing like seeing the attack work to understand why the defense matters.
One question I want to leave listeners with. We've talked about how CORS is a server-side opt-in mechanism for relaxing the Same-Origin Policy. But as web applications get more complex and more distributed, are we going to see a shift toward different security models entirely? Things like Content Security Policy and the various cross-origin isolation headers are already adding new layers. Is CORS going to be with us forever, or is it a transitional technology?
I think CORS fills a specific niche that's going to be relevant as long as browsers run untrusted JavaScript from multiple origins. The fundamental problem — "how do I let this script from origin A read data from origin B without letting every script do that" — isn't going away. The mechanism might evolve, but the need for explicit cross-origin permission isn't going anywhere.
Something to chew on. This has been My Weird Prompts. Thanks to our producer Hilbert Flumingtop. If you enjoyed this episode, leave us a review wherever you get your podcasts — it genuinely helps other people find the show. We'll be back soon with another one.