“The APIApplication Programming Interfacethe way two software systems talk to each other works when I test it manually, but the browser won’t touch it.” That sentence shows up in developer forums constantly. Word for word, sometimes. The tool outside the browser gets a clean response. The web page gets a red error in the console. Same APIApplication Programming Interfacethe way two software systems talk to each other. Same request. Different rules.

It’s the CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers error. It shows up in the console, usually in red, usually when you’re trying to ship something. The APIApplication Programming Interfacethe way two software systems talk to each other responds fine from tools outside the browser. No rules in the way. But the moment a web page tries to call it, everything falls apart.

The developer’s first instinct is almost always the same: “How do I turn this off?”

And that instinct is how most CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers misconfigurations are born. CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers stands for Cross-Origin Resource Sharing. It’s the browser mechanism that controls which websites can make requests to which servers.

The rule before the rule

CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers doesn’t add protection. It removes a restriction that was already there. To see why, you need to understand what it’s relaxing.

Browsers enforce something called the Same Origin Policy. This policy says that a web page can only make requests to the same “origin” it was loaded from. An origin is three things: the protocol (like https, the prefix that marks a secure connection), the domain (like example.com), and the port (like 443). Think of them as the street name, building number, and apartment number of a web address. If any of those three differ between where the page lives and where it’s trying to reach, that’s a different origin.

Think of it like a private event. The Same Origin Policy is the default rule: if your name isn’t on the building’s lease, you don’t get in. No exceptions. Every cross-origin request is treated as an uninvited guest until proven otherwise.

So if you’re on app.example.com and you try to call api.example.com, that’s a cross-origin request. The browser blocks it by default. Not because the request is dangerous. Because there’s no way to know yet if it’s safe.

CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers lets a server say “it’s okay, I’m expecting requests from that origin.” The server sends back specific headers telling which origins are allowed, which methods are permitted, and whether credentials like cookies can be included. CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers headers are the guest list. The server writes down which origins are welcome, and the browser checks the list at the door.

The Same Origin Policy is the security feature. CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers is the controlled way of poking holes in it. And that makes it surprisingly easy to poke the wrong ones.

Why the error feels backward

When developers see a CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers error, they think the server rejected their request. That’s not what happened. The browser rejected the response.

The request actually went through. The server processed it and sent back a response. But the response didn’t carry the right CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers permissions, so the browser hid it from the JavaScript code that made the request.

This feels wrong because it’s backward from how we usually think about access control. We expect the server to be the one deciding. With CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers, the server is just providing the guest list. The bouncer at the door, the browser, is the actual enforcer. Whether anyone checks the list depends entirely on who’s walking through the door.

That’s why CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers “works fine” when you test outside the browser. Those tools don’t enforce the Same Origin Policy. They don’t check CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers headers. They just send the request and show you the response. The CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers error is a browser behavior, not a server behavior.

The wildcard trap

The fastest way to make a CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers error go away is to set Access-Control-Allow-Origin: *. This tells the browser “any origin is fine.” And suddenly everything works.

Using the wildcard is like posting a sign on the door that says “open to the public.” That’s fine for a park, or a public weather APIApplication Programming Interfacethe way two software systems talk to each other, or a dataset endpoint where there’s no sensitive data and no user context.

But the wildcard has a critical limitation. If you set Allow-Origin: *, the browser won’t send cookies or other credentials with the request. The CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers specification explicitly prevents this combination. You can’t say “everyone is allowed” and also accept credentials, because that would let any website in the world make authenticated requests to your APIApplication Programming Interfacethe way two software systems talk to each other. You wouldn’t run an open-door policy at a private event where people are bringing valuables. The browser enforces the same logic. (If you’re curious why cookies carry so much weight, The Flimsy Wristband covers that.)

So developers who need credentials (which is most developers building actual applications) can’t use the wildcard. They have to specify which origins are allowed.

The real misconfigurations start here.

The guest list that writes itself

Some developers solve the credential problem by dynamically reflecting whatever origin the request came from. The server reads the Origin header from the incoming request and echoes it back in Access-Control-Allow-Origin. This way, every origin is technically “allowed,” but credentials also work.

This is like a bouncer who asks “what’s your name?” and then writes that name on the guest list. Everyone gets in. The list is meaningless. It’s functionally equivalent to a wildcard but without the browser’s safety check. It’s worse than * because it pretends to be specific while actually being completely open.

An attacker can create a malicious website, make requests to your APIApplication Programming Interfacethe way two software systems talk to each other, and the browser will happily include the user’s cookies because the server is confirming that the attacker’s origin is “allowed.” The attacker now sees whatever the authenticated user sees.

This pattern is everywhere. Forum answers, tutorials, production codebases that really should have caught it. Browser-based AI applications have made it even more common. Every startup building a frontend powered by an LLMLarge Language Modelan AI system trained to generate and understand text, like the models behind chatbots and coding assistantsLearn more in The Self-Signed Permission Slip → that calls a backend APIApplication Programming Interfacethe way two software systems talk to each other hits CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers for the first time. The reflect pattern is the first “fix” they find.

That’s the problem with a guest list anyone can write their own name on. It doesn’t fail loudly. It fails by making everyone feel safe.

The bouncer who calls ahead

For certain types of requests, the browser doesn’t just check the guest list at the door. It calls the venue first.

This happens when a request does something beyond the basics, like using a PUT or DELETE method (ways of updating or removing data on the server) or sending data in an unusual format. Before any of that goes through, the browser fires off an OPTIONS request (a lightweight check that asks the server “would you accept this type of request from this origin?”). Think of it as calling the venue to ask “can I bring a plus one who’s carrying equipment?” The host says yes or no before anyone walks through the door.

The problem is that every one of those calls doubles the traffic. That’s extra waiting time you can feel. And because many backend frameworks don’t handle OPTIONS requests by default, the bouncer picks up the phone and hears dead air. Silence. The errors that come back are baffling.

From there, the mistakes pile up. A method gets left out of Allow-Methods. A header goes missing from Allow-Headers. Nobody tells the bouncer to remember who already called, so the browser phones in every single time.

So CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers starts to feel like an obstacle, not a safeguard. And obstacles don’t get carefully configured. They get dismissed.

The guest list nobody actually writes

Most guest lists I’ve seen in the wild aren’t lists at all. They’re afterthoughts. A wildcard slapped on during development that nobody revisited. A reflect pattern copied from a tutorial that nobody questioned. The problem isn’t the guest list. It’s that nobody actually sat down and wrote names on it.

The guest lists that actually work have real names on them. Specific origins, not whatever name someone gives at the door. I keep seeing the wildcard in places where credentials are involved, where cookies are flying back and forth, where the door should absolutely not be open to the public. And every time, the team is surprised when I point it out.

Allow-Credentials: true only works safely with a specific, known origin. The wildcard and the reflect pattern both break this contract in different ways. One breaks it openly. The other breaks it while looking you in the eye and smiling.

Then there’s Access-Control-Max-Age, the header that tells the browser how long to remember a preflight result. Most teams don’t set it. So repeat visitors call ahead every single time, like showing up to the same party every night and having to prove they’re on the list again.

But here’s the part that trips up almost everyone I talk to: CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers isn’t protecting your APIApplication Programming Interfacethe way two software systems talk to each other. Your APIApplication Programming Interfacethe way two software systems talk to each other is reachable from anywhere. Any backend service, any script, any command-line tool that sends requests without a browser can call it without a CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers check ever happening.

CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers is protecting the user. It’s preventing malicious websites from making requests with the user’s credentials without the user’s knowledge. And most teams have never thought about it that way.

The door that only covers one entrance

CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers only works because browsers agree to play by the rules. The guest list only matters if everyone comes through the front door. A script doesn’t check the list. Neither does a backend service running on a server somewhere. And increasingly, the things calling your APIApplication Programming Interfacethe way two software systems talk to each other aren’t browsers at all. AI agents, automated pipelines, scrapers. None of them see a CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers header in their lives.

The bouncer only works at one door. And the building has more entrances every year. As more of the web shifts toward machine-to-machine communication, CORSCross-Origin Resource Sharingthe browser mechanism that decides which websites can call which servers protects a shrinking slice of the ways your application can be reached. It still matters for the browser-based world. But that world is getting smaller. And the things slipping through the other entrances don’t check guest lists, don’t announce themselves, and don’t care that there’s a bouncer at all. I’m not sure anyone’s counting the ways in anymore.

Previously on Off White Paper: The Flimsy Wristband explored why session cookies are the thing attackers want most, and The Self-Signed Permission Slip unpacked how JWTsJSON Web Tokena compact, self-contained token that carries identity between systemsLearn more in The Self-Signed Permission Slip → carry trust without checking back in. This post looks at the browser mechanism that decides whether those credentials get sent across origins at all.