The API only allows https://example.dev cross-origin access, but an attacker can still directly request it using the command line. CORS (Cross-Origin Resource Sharing) is a browser-implemented restriction on access, not a server-side authentication mechanism. Relying solely on Origin checking as a permission control would make it easy for non-browser clients to bypass.
This article discusses web API calls, particularly requests with cookies. Public data interfaces and private account interfaces should use different contracts and should not share a single “allow all sources” middleware.
First, observe the same operation using three different clients.
Assume an API that changes a nickname relies on a cookie-based session. The site’s own page can send a request and read the result. A page on another site may also send a request under some conditions, but the browser can refuse to let that page read the response. Command-line clients are not subject to browser CORS checks. An attacker can forge an allowed Origin header in a non-browser client, so checking that header alone is not authentication. The API must verify the session and permission for the operation rather than derive the user’s identity from cross-origin headers.
Request arrival and response readability are distinct events. A cross-site form that does not require a preflight can change state even if the attacking page cannot see the result. Security acceptance tests should check server-side side effects, not treat a cross-origin console error as proof that a write was blocked. CSRF concerns a browser sending credentials with an operation that the user did not intend to authorize. That is a different responsibility from deciding whether a script may read the response.
For publicly accessible indexes, any website reading them is perfectly reasonable. In this scenario, wildcard sources can express public policies, but all fields must be publicly accessible, and there should also be a budget for traffic costs. It’s important not to simplify the configuration of this public interface and then apply the same rules to account interfaces. Separating contracts based on data sensitivity is easier to review than establishing a middleware that automatically reflects the origin for all routes.
Determining Who Can Access the Response: The Server’s Role
const allowedOrigins = new Set(['https://example.dev']);
const origin = request.headers.get('Origin');
const headers = new Headers(response.headers);
headers.delete('Access-Control-Allow-Origin');
const vary = (headers.get('Vary') || '')
.split(',').map((field) => field.trim().toLowerCase());
if (!vary.includes('*') && !vary.includes('origin')) {
headers.append('Vary', 'Origin');
}
if (origin && allowedOrigins.has(origin)) {
headers.set('Access-Control-Allow-Origin', origin);
}
This is a response-header processing fragment, not an authentication implementation. The variable response is the response about to be returned. Clone its existing headers, remove any leftover origin permission, and add the Origin variant on every branch. Preserve existing fields such as Accept-Encoding and preserve Vary: *; do not add Origin again if it is already present with different capitalization. Apply the same processing to default, denied, error, and 304 responses. Do not first cache a default response for a request without Origin and then let an allowed origin reuse it without the appropriate variant handling. Reconstruct the returned response with these headers while preserving its status and valid body rules. Credentialed access also needs an explicit credentials policy: Access-Control-Allow-Origin: * is not a general-purpose combination for credentialed requests. Exact origin matching includes the scheme and port; substring matching can accidentally allow lookalike domains.
Vary: Origin tells caches that responses vary by origin, but verify the platform’s actual cache behavior as well. If different origins receive different CORS headers while sharing a single cache object, legitimate requests can fail or unintended read paths can appear.
Origin is defined by the protocol, hostname, and port. The path does not belong to the origin. Once an origin is allowed, any script on that origin can make requests to the interface, including injected scripts. The origin whitelist should be obtained from a controlled configuration and should not use simple “contains” or regular expression matching for hostnames; similar domain names and unregistered ports should be rejected. A null origin may appear in a sandbox or file scenario and should not be used as a general default allow value for all unknown origins.
If the business supports customers using custom domain names, the whitelist must be associated with validated tenant domains and remain revocable. Do not directly store the source from client requests in the whitelist. After source verification, it is still necessary to check which user and tenant the request belongs to, and whether the user has permission to access the resource. Cross-origin authorization only states that the browser allows scripts to read, but it does not mean that every user in the source can read all accounts.
When responding to requests with dynamic source headers, the cached variations must also be correct. If the public response object is polluted by the headers from the first source, the second legitimate source may not be able to read it. When adding Vary, it is important to preserve existing fields and not overwrite Accept-Encoding or other variations. Private responses require correct private caching strategies, and should not be placed in shared objects simply because the CORS headers appear strict.
Not Every Request Has a Preflight
The browser performs OPTIONS preflight requests to check compatibility with certain methods, headers, and content types. Requests that meet the simple request criteria may be sent directly, and even if the server does not allow reading, side effects may have already occurred. Therefore, the statement “You won’t receive cross-origin requests if CORS is not configured” is a misunderstanding.
When handling preflights, restrict allowed methods and request headers and return an appropriate cache lifetime. Do not reflect every header name requested by the client into the allowlist. Preflight caching affects troubleshooting: after changing the configuration, test in a fresh browser context rather than mistake an old cached result for evidence that production has not updated.
The OPTIONS request is used to query whether cross-origin methods and headers are allowed. It should not be used to perform actual business writes in a pre-check branch. Typically, the pre-check does not carry the same authentication credentials as the actual request, and should not simply reject all pre-checks after applying session authentication. Instead, it can return allow information based on the source and destination routing policies, and the actual request can then be re-authenticated and authorized. A successful pre-check does not guarantee permanent authorization for future requests; the server still processes each operation.
The methods should come from the interface definition, and the request headers should come from the fields that are actually needed. They should not reflect any arbitrary values from the client’s request. Returning a reasonable pre-validation cache expiration can reduce redundant requests, but it will delay the client from observing configuration changes. Browsers may also impose their own limits. After modifying the policy, test in a new browser context and simultaneously check the CORS headers of the actual request response. If the pre-validation only returns success while the business response is missing headers, it will still lead to a read failure.
Custom response headers must be exposed to scripts according to browser rules. The fact that the network panel can see these headers does not mean that the fetch code can read them. If download names, request IDs, and pagination information are returned through headers, the corresponding fields must be explicitly exposed. Internal diagnostic, authentication, or unnecessary server details should not be exposed. The frontend data protocol includes both the body and readable headers, and it is not possible to only validate one of them.
Identity and CSRF have independent responsibilities
Authentication determines who a request belongs to; authorization determines what that identity may do. Cookie-based sessions also need CSRF protection because a browser may automatically attach the session to a request induced by an attacking page. Choose SameSite settings, CSRF tokens, and origin verification to suit the application architecture. Do not make CORS responsible for write protection that it does not promise to provide.
Bearer tokens are not a universal solution. If the token is stored in a location accessible to injected scripts, the risk of XSS increases. The chosen authentication scheme should be based on the threat model and client conditions, rather than simply changing private interfaces to anonymous ones due to CORS configuration difficulties.
When a browser calls different source APIs, it needs to send cookies based on the fetch credentials policy. At the same time, the server must return the specific allowed sources and credential permissions. Even if all of this is correct, the SameSite attribute of the cookies and browser-level third-party restrictions may prevent their transmission. CORS does not force browsers to abandon their cookie privacy policies, and when troubleshooting, it is important to check the transmission conditions, API authentication, and response reading separately.
Do not change every cookie to SameSite=None merely to make cross-site calls convenient. First consider whether a same-origin proxy or an explicit authentication architecture can reduce reliance on cross-site sessions. Clients send bearer tokens explicitly, which gives them different CSRF characteristics, but token storage and XSS risks still need attention. Choose authentication based on the threat model, not solely to eliminate a preflight.
For write interfaces that require CSRF protection, use architecture-appropriate tokens, along with SameSite and controlled source verification. Source verification can participate in protection because the browser provides relevant context, but it remains separate from server-side authentication. For background tasks, use independent service credentials, without allowing anonymous writes to maintain compatibility with environments without a defined origin. Multiple client types should have distinct, understandable authentication paths.
Don’t Mask Server Issues with Broad CORS
Upstream anomalies can cause the proxy to return a 500 response without the necessary CORS headers, while the browser only reports a failed cross-origin read. Developers then relaxed the source rules without fixing the underlying problem. The same source policy should be applied to all error responses that require client access, while maintaining the original state and security content. This allows the frontend to display controlled errors, rather than just generic network errors.
First, verify that the request was actually sent, that the pre-check was successful, and that the actual response is what you expect. Also, check if the browser is allowed to read the response. Server-side logs can confirm that the request reached the server and the authentication result. The browser’s network panel can confirm the protocol behavior. Avoid exposing internal error details to external clients solely for the purpose of reading logs. Correlating identifiers will align the two perspectives and help identify the root cause.
Cross-origin proxies also make it easier to bypass existing boundaries. Development servers provide proxies for convenient local debugging, but when the production frontend directly requests upstream resources, CORS issues will reappear. It is crucial to clearly define whether the production call path uses a same-origin worker or a cross-origin API. Do not assume that the successful use of a development proxy in a production environment has already established the desired architecture. If a same-origin proxy is used, the proxy itself still requires authentication, authorization, and upstream range restrictions.
The source policies can be versioned, and the registered entries must be verified during deployment. When migrating domains, it is necessary to plan the addition of the new source and the revocation of the old source. Maintaining all historical preview addresses will continuously expand the allowed scope. Short-term preview domain usage is governed by limited and controlled rules or independent testing interfaces, and arbitrary preview hosts created by any user are not authorized to the production account API.
For untrusted embedded pages, “null source” is not a specific, trustworthy website, and multiple different sandbox scenarios may use this expression. If the product truly needs it, data and operations should be restricted, and independent authentication should be implemented, rather than treating it as a broad compatibility switch. Each additional allowed source should clearly state who controls it, why it is needed, and when it will be revoked. A clear read policy combined with accurate permission checks is more reliable than a string of entries intended to eliminate console errors.
Trace an Attack and a Legitimate Request Through One Session-Based Update
The user has already logged in. The external website page prompts the browser to send a request to the nickname interface. If the request meets the conditions that do not require pre-validation, the server may directly receive a request with identity context. Whether the specific cookie is sent depends on the relationship between the site, its attributes, and browser policies. The response is rejected by CORS, but this does not necessarily prove that the write operation has not occurred. Security testing requires querying the actual nickname or a controlled counter to confirm whether the server has accepted the operation.
When a legitimate page on this site sends the same operation, the server first verifies the session, permissions, and applicable CSRF conditions, performs the write, and then returns a readable response according to its origin policy. Choose CSRF defenses for the session architecture; combining a few example rules arbitrarily is not a general security guarantee. Bind tokens to the correct session and validation flow, match origins precisely, and choose SameSite settings for the actual cross-site requirements. Each defense has its own scope, and a CORS read failure cannot replace any of them.
A non-browser client can send a forged Origin header, but that header alone cannot give it an identity. It still needs valid authentication and permission; background jobs should use separate service credentials. If the API checks only Origin, testing with this client will expose the vulnerability. Do not disable all identity checks to accommodate clients without Origin, and do not automatically treat a missing Origin as failed authentication. Define client types and authentication methods separately so compatibility fixes do not make the rules inconsistent.
Use a Request Ledger to Separate Preflights from Real Operations
A preflight asks which methods and headers the route permits; it does not perform the nickname change. In a test upstream, record separate synthetic counters for OPTIONS and POST, and assert that repeated preflights never increase the business-operation count. After a successful preflight, the actual request still needs authentication. A client cannot treat that allowance as a long-term authorization credential. The browser’s preflight cache reduces policy queries; it does not let the server skip business authorization.
A failed preflight may prevent the actual request from being sent, while a simple request may reach the server without a preflight. The frontend can display a cross-origin error in both cases, but the server ledger will differ. Record request arrival, authentication outcome, business execution, and response readability as separate observations to explain the result accurately. Logs should contain only categories and correlation identifiers, not cookies, tokens, or complete private bodies.
Preflight failures can also come from a missing route or a service outage. Inspect the status and actual response before checking whether the allowed headers match. Do not “fix” a server failure by reflecting more origins. Error responses for allowed clients need a consistent origin policy so the frontend can read a safe error category. Being allowed to read a controlled error does not entitle a client to an internal stack trace; keep the error representation minimal.
Customer Domains Need Revocable Origin Mappings
When a multi-tenant product lets customer domains call its API, verify domain ownership before registering a domain to the tenant. Check three things separately: whether the request Origin matches an allowed origin, whether the authenticated user belongs to the tenant, and whether that user may act on the target resource. Allowing a domain does not let all users on it read another tenant’s data, nor make that domain permanently trustworthy. Remove its origin mapping when the binding is revoked or the account is deactivated.
Precise sources include protocols and ports. Wildcard subdomains may cover customer-controlled sites or legacy services, and should not be used to grant access to the entire domain tree for convenience. The preview environment’s sources should also be separate from the production environment. Any preview pages created by external contributors should not be granted production account read permissions. Choose a limited, controlled preview or testing API to reduce the long-term permissions associated with a constantly growing domain.
Source verification allows read policies while restricting data access based on identity authorization. Testing two tenants with their respective legitimate domains and users, and then cross-combining them, confirms that requests originating from legitimate sources but with mismatched tenants are still rejected. The server-side interface remains usable for authorized backend calls without an “Origin” header, indicating that security comes from authorization rather than solely relying on browser headers. This cross-matrix approach is closer to a real-world boundary test than a single test case for a source string.
The cache header must be consistent with both public and private representations.
A public document index might allow all websites to access it, while private account results cannot be stored in the same shared cache object. Dynamic reflection of the Origin header requires proper handling of variations. Existing Vary fields cannot be overridden. The platform must verify that the corresponding variations are correctly implemented in a real deployment, especially with manual caching or custom keys. The protocol declaration and the platform implementation are not the same level of guarantee.
Request from origin A and then origin B, and repeat in reverse order, checking for contamination of the returned headers. Then change the authenticated user and confirm that private bodies are not reused for another user. Valid CORS headers do not make a mixed-up account response safe: reusing another user’s body remains a serious vulnerability. The browser checks origin permission, not whether the server returned the correct account. Cache eligibility, identity isolation, and read policy must all hold together.
Do not relax permissions when third-party cookies are restricted for diagnosis
Even with the right request configuration and server permission for credentials, a browser may block cross-site cookies. Check which cookies were actually sent, their SameSite and Secure attributes, and current browser restrictions. Reflecting arbitrary origins cannot fix a missing session. A same-origin proxy may reduce cross-site dependencies, but it still needs full authentication, authorization, and upstream restrictions; it is not an automatically secure compatibility layer.
Bearer authentication changes credential transport and introduces token-storage and XSS-protection responsibilities. Do not switch authentication schemes just because preflights are inconvenient. Record the trusted execution environment and threat model before choosing an approach. Final acceptance must demonstrate both successful legitimate tasks and rejection of unauthorized side effects. The absence of cross-origin console errors is only one compatibility result, not the security acceptance criterion.
Download Redirects Bring a Second Server into the Read Contract
After the report interface authentication is successful, it returns an object storage download address. The frontend uses fetch to follow and navigate, but ultimately encounters a read error. To resolve this, you need to examine the complete request chain: The CORS headers of the initial API endpoint cannot guarantee that the final storage response will allow reading. The final goal must comply with the reading rules of the corresponding browser. The credentials of the original endpoint cannot be assumed to grant access to all targets.
First, it’s important to clarify whether the delivery method involves the script reading content or directing the user to a controlled download URL. These two methods use different browser protocols, and assuming that “opening a new tab allows downloading” is compatible with fetch is incorrect. If a signed URL is used, it has its own validity period, scope of application, and potential for leakage. It does not print the complete query parameters in public logs, and it does not open the entire storage space to resolve read errors.
For isolation acceptance, use an object with no sensitive content. Record the response from the entry point, the target category for redirection, and the final status. Also, test both valid and expired authorizations separately. The results report distinguishes between API rejections, storage rejections, and browser-unreadable states. This document does not include a real-world vendor download experiment; the project still requires verification of its signature and response header capabilities. Changing the delivery method does not bypass the user’s report ownership check.
Testing “Being Read” and “Being Executed”
Prepare the website pages, external test pages, and command-line client. Observe whether requests reach the server, whether operations are executed, and whether the browser allows scripts to read. The logs should be able to distinguish between these three different results.
The tests should include scenarios without an Origin, null Origin, similar domains, different ports, and unauthorized users. Command-line requests without an Origin must still be authenticated. Do not completely reject background tasks solely for browser validation, and do not allow anonymous access solely to accommodate background tasks.
Public API: If a website is allowed to read data, using wildcard sources can be a reasonable option, but it’s still important to restrict abuse, control costs, and protect sensitive fields. The CORS configuration should describe the cross-origin read policy. API security should be ensured through authentication, authorization, input validation, and rate limiting.
A common “fix” is to change fetch to mode: 'no-cors'. This is not a switch for bypassing server permission rules and obtaining the body. A cross-origin request produces an opaque response that does not give scripts normal access to its status, headers, or body; request methods and headers are restricted too. If the task is to parse account JSON, this mode has not achieved it. Tests must check that the script actually obtains the expected fields, not merely that its Promise avoids the error branch.
The Set-Cookie response in a login process represents another boundary: While browsers can handle cookies according to rules, they prevent front-end scripts from accessing this header through the response header interface. Adding the header to a list of exposed headers does not make it a readable session field. Whether a session is established should be verified by subsequent, controlled authentication requests and the browser’s storage behavior, rather than returning the cookie value to JavaScript. Reading the response, browser storage, and subsequent authorization remain three independent results.
References: MDN CORS, Request.mode, Set-Cookie, OWASP CSRF Protection.