“The 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 . Same request. Different rules.

It’s the error. It shows up in the console, usually in red, usually when you’re trying to ship something. The 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 misconfigurations are born. 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

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.

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. 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. 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 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 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 , 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 “works fine” when you test outside the browser. Those tools don’t enforce the Same Origin Policy. They don’t check headers. They just send the request and show you the response. The error is a browser behavior, not a server behavior.

The wildcard trap

The fastest way to make a 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 , 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 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 . 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 , 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 that calls a backend hits 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 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: isn’t protecting your . Your is reachable from anywhere. Any backend service, any script, any command-line tool that sends requests without a browser can call it without a check ever happening.

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

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 aren’t browsers at all. AI agents, automated pipelines, scrapers. None of them see a 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, 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 carry trust without checking back in. This post looks at the browser mechanism that decides whether those credentials get sent across origins at all.