The same-origin policy is not protecting your API
A permissive CORS header delegates the read decision to the requester, letting attacker script read authenticated responses through the victim's own browser.
Opening Claim
Developers expose APIs to the public internet without understanding the control that governs who is permitted to read the response. That control is CORS. The failure is consistent. It is not occasional, not an edge case, not the product of an advanced adversary. It is a security awareness gap that produces an immediate, externally reachable attack vector.
This is not a theoretical problem. An endpoint that answers cross-origin requests with sensitive data, and instructs the browser that any origin is permitted to read that data, is exploitable the moment it is live. No malware. No supply chain compromise. No zero-day. The mechanism is the browser doing exactly what the server told it to do.
The scope here is narrow. This is not a lesson on how CORS works. It is an account of what happens operationally when CORS is deployed by someone who does not understand it. The open endpoint is the evidence. The exploitation path is the proof. Readily available tools close the gap between an exposed endpoint and an attacker reading its responses.
The Original Assumption
The assumption is that the API is protected because it sits behind the browser’s same-origin policy. Developers treat the same-origin policy as a server-side wall. It is not. It is a browser-enforced restriction on what client-side script is allowed to read across origins. The server is not the enforcement point. The browser is. CORS is the set of response headers the server uses to relax that restriction.
From this assumption a second one follows. CORS is experienced as an obstacle, not a control. It surfaces as a blocked request and a console error during development. The fastest way to make the error disappear is to return a permissive header: Access-Control-Allow-Origin set to wildcard, or the requesting origin reflected straight back. The error clears. The feature works. The developer moves on. The control has now been disabled, and the disabling was recorded as a success.
The third assumption is about value. The endpoint is judged harmless because the developer does not classify the data it returns as sensitive, or does not believe an external origin can reach it in a way that matters. This is the awareness failure stated plainly. The boundary that was loosened is the boundary that decides whether an attacker’s web page can read authenticated responses on behalf of a victim. The assumption that this is a convenience setting is the assumption that fails.
What Changed
What changed is the enforcement location, and the developer did not account for it. The check assumed to live on the server lives in the victim’s browser. When the server returns a permissive CORS header, it is instructing every visitor’s browser to hand cross-origin response data to whatever script requested it. The attacker does not breach the server. The attacker supplies the script and lets the victim’s browser perform the read.
The exploitation path is direct and uses tools that require no privileged access. A request to the endpoint carrying an arbitrary Origin header reveals the server’s response headers. If the server returns Access-Control-Allow-Origin matching that arbitrary origin, or a wildcard, the boundary is confirmed open. curl confirms it in a single request. A few lines of JavaScript hosted on an attacker-controlled page complete it. Where credentials are in scope and the origin is reflected, that page reads authenticated data using the victim’s own session.
This converts a passive coding choice into an active attack vector with no further escalation required. The endpoint left open is not waiting for a sophisticated operator. It is reachable by anyone who can read a response header and write a fetch call. The operational impact is the point. The negligent use of CORS does not create a weakness that might be exploited later under specific conditions. It creates one that is exploitable now, by default, with standard tooling.
The Mechanism
The mechanism is delegation of an access decision to a value the requester controls. The endpoint receives a request carrying an Origin header. That header is supplied by the client. When the server reflects that origin into Access-Control-Allow-Origin, or returns a wildcard, it has made the requester’s own input the basis of the authorization to read the response. The server is not deciding who may read. It is repeating what it was told and recording that as a decision.
The enforcement point sits in the victim’s browser, not on the server. The server emits a header. The browser reads that header and decides whether to release the cross-origin response body to the script that requested it. Every step is externally observable: the request carrying an arbitrary Origin, the response header that matches it, the browser releasing the data to attacker script. Nothing in that chain requires server compromise. The server behaves exactly as configured. The configuration is the failure, and the failure is the boundary, not a defect adjacent to it.
Two conditions convert this from a loose setting into a working read. The first is reflection of the requesting origin, or a wildcard, in the response header. That alone makes whatever the endpoint returns readable cross-origin. The second condition applies where credentials are in scope: the origin is reflected, the browser attaches the victim’s session to the cross-origin request, and the server still authorizes the read. When both hold, an attacker-controlled page issues a fetch, the victim’s browser sends the session, the server returns the permissive header, and the authenticated response body crosses to script the attacker wrote. The boundary assumed to be the same-origin policy was never enforcing on the server. It was the browser’s restriction, and the server told the browser to drop it.
The Pattern, Generalized
The failure is broader than one header. The mechanism is a server treating requester-supplied input as a trusted authorization signal, while the enforcement of that trust lives somewhere the server does not control. CORS is one instance of it. The Origin header is the requester-supplied input. The browser is the enforcement point the server does not own. Remove the protocol specifics and the shape holds: a value the caller controls is reflected back into the decision that governs the caller’s access.
Reflection is the constant, and it is where the appearance of a control diverges from its effect. A wildcard hands the read to every origin. A reflected origin hands the read to whichever origin asked, which is the same outcome reached through a different sentence. Both instruct the browser to release the response to the caller. The difference between them is cosmetic. An operator who replaced a wildcard with origin reflection to look more restrictive changed how the control reads, not what it does. Naming the requester is not validating the requester. The boundary is open in both forms.
The defining property is not the header name. It is the location of enforcement and the source of the value being trusted. When the trusted value originates with the requester, and the enforcement sits outside the server, the server holds no authority over who is permitted to read. It holds a setting that produces the appearance of one. Any configuration with that property carries the same exposure as the open endpoint already described, because it is the same mechanism, not a comparable one. The attacker controls the input that decides the attacker’s access. That is the pattern, stated without the wrapper it arrives in.
Operator Position
A control that lets the requester authorize their own read is not a control. Access-Control-Allow-Origin set to wildcard, or set by reflecting the Origin header, does not restrict access. It declares access unrestricted, in a format the browser obeys. Labeling it a security header does not make it enforce anything. Where credentials are in scope and the origin is reflected, the endpoint serves authenticated response data to attacker script running inside the victim’s browser. That is the confirmed condition. It is not a projected risk and it is not contingent on a future event.
What must now be true is that the read decision belongs to the server and is made against values the server controls, not against the Origin the requester supplied. The enforcement point is the question. The same-origin policy was never the server’s wall. It is the browser’s, and the browser releases whatever the server’s headers permit. An endpoint returning sensitive or credentialed data is defined by the exact header it emits and the exact origin it authorizes. If that header reflects the caller or wildcards the field, the boundary is open and the control is absent. Not weak. Absent.
The wider condition is the one that holds without exception: if a system allows it, it will happen. An open CORS response is not a latent risk waiting on a sophisticated operator. It is reachable by anyone who can read a response header with curl and write a fetch call. The cost of discovery is one request. The cost of exploitation is a static page. The exposure does not scale with attacker skill. It scales with the count of endpoints shipped by developers who read a browser error as the problem and a permissive header as the fix. Until that gap closes, the boundary stays open by default, and default is where most of these endpoints live.
Keep Reading
open source security toolsEight months building a Burp Suite replacement
An honest write-up of building Interceptor, an open-source Burp Suite alternative - license choices, attacker math, defender economics, and what got cut.
A favicon is a code execution primitive.
How attackers hide skimmers and full payloads in favicon files, why MIME and CSP misconfiguration lets image bytes run as code, and what defenders miss.
ios privacyA loupe over the data iOS never asked about
Loupe shows iOS gates a fixed sensor list and leaves contextual device data readable by default, with no prompt, no consent, and no record of access.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.