
Cross-Origin Resource Sharing (CORS) is how a server tells a browser which other origins may read its responses. It is a browser-enforced relaxation of the same-origin policy, not an access control on the server, so it does nothing to stop requests from tools that are not browsers. The main risk is a policy that reflects any origin back while also allowing credentials, which can let another site read authenticated responses in a signed-in user's browser.
Check your CORS response headers
Paste curl -s -D - or DevTools response output, including the HTTP status line. Avoid curl -I, which sends HEAD and can return different headers. For a redirect chain, each response is shown separately, in pasted order. These are response blocks, not proof of distinct network hops.
Pasted text stays in your browser. Nothing is sent or stored.
Used only to check for possible origin reflection. You can also leave this blank and include the request Origin in a curl -v paste. Nothing is sent.
Only pasted text is checked, not your live site. A truncated capture may omit fields. Check successful, preflight, redirect and error responses separately. A tight CORS policy is one layer among several and is not security assurance.
Checks cite the Fetch Standard (CORS protocol and credentials, the CORS check, HTTP caches, and CORS-preflight fetch) and MDN documentation for Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, and Access-Control-Allow-Headers.
Short answer
A safer CORS default is an explicit allowlist of exact origins, plus Vary: Origin, and no wildcard with credentials. Use Access-Control-Allow-Origin: * only for public, read-only data that needs no cookies or other credentials. The riskier pattern is reflecting any Origin back with Access-Control-Allow-Credentials: true, which can let another site read authenticated responses in a signed-in user's browser. Pairing * with credentials does not work in browsers, and teams who hit that error sometimes switch to reflecting the origin.
CORS header config generator
Pick your setup to get a CORS config snippet and a curl check. Everything runs in your browser and nothing is sent anywhere. Output is illustrative, so review it against your own setup before applying it.
# http block of nginx.conf (illustrative)
# Safer: exact allowlist, anything else gets no CORS header
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
# server or location block
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Vary Origin always;
if ($request_method = OPTIONS) {
return 204;
}# Allowed origin
curl -s -o /dev/null -D - -H "Origin: https://app.example.com" https://api.example.com/endpoint
# Untrusted origin
curl -s -o /dev/null -D - -H "Origin: https://evil.example" https://api.example.com/endpoint
# Expected for the untrusted origin: no Access-Control-Allow-Origin headerTroubleshooting notes
| What you see | Likely cause |
|---|---|
| Untrusted origin comes back in Access-Control-Allow-Origin | The policy reflects origins, or the allowlist match is too loose. |
| No CORS headers for an allowed origin | Config not reloaded, or the Origin does not match exactly (scheme, host and port). |
| Two Access-Control-Allow-Origin headers | Both the app and the proxy add the header. Set it in one place. |
| Browser error about * with credentials | A wildcard origin cannot be used with credentials. |
| Preflight OPTIONS request fails | OPTIONS is not handled, or the method or header is not in the allowed list. |
Safer and riskier CORS setups
The two headers that matter most are Access-Control-Allow-Origin and Access-Control-Allow-Credentials. This table compares common setups.
| Setup | Risk | Why |
|---|---|---|
| Access-Control-Allow-Origin: * without credentials | Generally acceptable for public data | Fine for truly public, read-only data. Browsers send these requests without credentials, so cookies and other credentials are not included. |
| Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true | Does not work | Browsers reject this combination and do not share the response. Teams who hit this error sometimes switch to reflecting the origin, which is riskier. |
| Reflecting any Origin header back, with Access-Control-Allow-Credentials: true | High | Any site can read authenticated responses in a signed-in user's browser. |
| Allowing the null origin | Risky | Sandboxed iframes and some file contexts send Origin: null, so allowing it can let untrusted content read responses. |
| Loose suffix or prefix matching | Risky | A check that accepts any origin ending in example.test also accepts https://attackerexample.test. A prefix check on https://example.test also accepts https://example.test.evil.example. |
| Explicit allowlist with exact match, plus Vary: Origin | Safer pattern | Only listed origins get the header, and Vary: Origin keeps caches from serving one origin's response to another. |
Riskier
Reflecting any Origin header back, with Access-Control-Allow-Credentials: true
Request
Origin:
Response
Access-Control-Allow-Origin:
Access-Control-Allow-Credentials:
Any site can read authenticated responses in a signed-in user's browser.
Safer pattern
Explicit allowlist with exact match, plus Vary: Origin
Request
Origin:
Response
Access-Control-Allow-Origin:
Access-Control-Allow-Credentials:
Vary:
Only listed origins get the header, and Vary: Origin keeps caches from serving one origin's response to another.
CORS is not a CSRF defense
A permissive CORS policy does not protect you from Cross-Site Request Forgery, and a strict one is not a substitute for anti-CSRF tokens. Treat them as separate controls. Anti-CSRF tokens remain your first line of defense against forged requests, and CORS controls who can read responses.
Configure it safely
Prefer an explicit allowlist of the exact origins that need access, and avoid pairing wildcards with credentials. A safer response looks like this:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: OriginSet the origin per request from a known allowlist rather than echoing it blindly, and include Vary: Origin so caches do not serve one origin's response to another.
How to check your own API
You can run a quick self-check against an API you own. Send a request with an origin you do not trust and read the response headers:
curl -s -o /dev/null -D - -H "Origin: https://evil.example" https://api.example.com/endpointThis sends a normal GET request and prints only the response headers. It avoids -I, which sends a HEAD request, because some APIs route HEAD differently from GET and can return different headers.
Look for Access-Control-Allow-Origin and Access-Control-Allow-Credentials in the response. If the untrusted origin comes back with credentials allowed, the policy is reflecting origins.
Illustrative risky response:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: trueIllustrative safer response, with no Access-Control-Allow-Origin header for the unknown origin:
HTTP/1.1 200 OK
Content-Type: application/json
Vary: OriginRepeat the check with -H "Origin: null", and with lookalike hostnames such as https://attackerexample.test for an API on example.test, to catch null-origin and loose matching rules. Check each endpoint that returns sensitive data, since policies are sometimes set per route.
Fix checklist
- Use an explicit allowlist of exact origins, including scheme and port, and compare origins exactly.
- Return
Access-Control-Allow-Originonly for origins on the allowlist, and send no CORS headers for anything else. - Send
Access-Control-Allow-Credentials: trueonly where cross-origin credentialed requests are actually needed. - Do not allow the
nullorigin. - Use
*only for public, read-only data that needs no credentials. - Add
Vary: Originwhenever the header value changes by origin. - Re-run the self-check after each change, and keep authentication and anti-CSRF controls in place, since CORS is not an access control.
A tight CORS policy reduces risk, but it is one layer among several. A misconfigured policy is a common finding in a web application security test. When it shows up in a report, look for the observed headers, the impact of reflecting Origin with credentials, and a concrete fix such as an exact allowlist. See how to read a penetration test report for what a usable finding includes. A penetration test is not a compliance certification. If you would like us to review your API's CORS policy along with the rest of the application, we can include it in a scoped web application test against a scope we agree together. Contact us to get started.
Frequently asked questions
Is CORS a CSRF defense?
No. A permissive CORS policy does not protect you from Cross-Site Request Forgery, and a strict one is not a substitute for anti-CSRF tokens. Treat them as separate controls. Anti-CSRF tokens remain your first line of defense against forged requests, and CORS controls who can read responses.
How do I check CORS with curl or DevTools?
Against an API you own, send a GET with an origin you do not trust and read the response headers. For example: curl -s -o /dev/null -D - -H "Origin: https://evil.example" https://api.example.com/endpoint. Avoid curl -I, which sends HEAD and can return different headers. In the browser, open DevTools, reload, and look at Access-Control-Allow-Origin and Access-Control-Allow-Credentials on the response. If the untrusted origin comes back with credentials allowed, the policy is reflecting origins. Repeat with Origin: null and lookalike hostnames.
Does hiding Origin help?
Hiding or stripping the request Origin does not fix a policy that reflects origins or pairs a wildcard with credentials. The safer pattern is the opposite of reflection: compare the request Origin to an exact allowlist, return Access-Control-Allow-Origin only for listed origins, and send no CORS headers for anything else. Include Vary: Origin so caches do not serve one origin's response to another.
Is Access-Control-Allow-Origin: * with credentials a safer default?
No. Browsers reject Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true and do not share the response. Teams who hit that error sometimes switch to reflecting the Origin, which is riskier. A wildcard without credentials is generally acceptable for truly public, read-only data. The safer default for authenticated APIs is an explicit allowlist of exact origins and no wildcard with credentials.

Have a system you want tested?
Tell us what you are working with and we will follow up with next steps.