How to check CORS configuration
Read every access-control-* header the API returns for a real origin: curl -s https://api.example.com/orders -H 'Origin: https://app.example.com' -D - -o /dev/null. A configured API returns your exact origin, a vary: origin line, and no CORS header at all for an origin outside its list.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
An audit of the CORS configuration belongs on staging sign-off, after any change to the API gateway, and before a new front end host goes live. One command tells you whether the API holds a list of origins or copies back whatever arrives. A service that copies the origin and also sends access-control-allow-credentials: true lets any page the tester's browser opens read that tester's logged-in responses, and no status code in the test report shows it.
Prerequisites
- curl 7.0 or later. See the curl manual.
- Node 18 or later for step 4, which runs a reference server with a real allow list on port 8099.
- The MDN CORS reference for the meaning of each header.
- Save the reference server as
cors-allowlist.jsbefore step 4:
const http = require('node:http');
const allowed = new Set(['https://app.example.com', 'http://localhost:3000']);
http.createServer((req, res) => {
const origin = req.headers.origin;
res.setHeader('Vary', 'Origin');
if (origin && allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Expose-Headers', 'X-Request-Id, X-RateLimit-Remaining');
}
res.setHeader('X-Request-Id', 'req-42');
res.setHeader('X-RateLimit-Remaining', '59');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end('{"ok":true}');
}).listen(8099, () => console.log('listening on 8099'));
Steps
- Step 1.
Read the CORS headers the API returns for an origin that belongs to you.
curl -s 'https://httpbin.org/get' -H 'Origin: https://app.example.com' -D - -o /dev/null | grep -i -E 'access-control|vary|^HTTP'HTTP/2 200 access-control-allow-origin: https://app.example.com access-control-allow-credentials: trueThree facts are already visible: the origin is allowed, cookies are allowed with it, and there is no
varyline namingOrigin. - Step 2.
Repeat the request with an origin nobody in the project owns.
curl -s 'https://httpbin.org/get' -H 'Origin: https://evil.example' -D - -o /dev/null | grep -i -E 'access-control|vary|^HTTP'HTTP/2 200 access-control-allow-origin: https://evil.example access-control-allow-credentials: trueThe header follows the request, so there is no allow list here. Reflection plus credentials is the finding to file.
- Step 3.
Find out which response headers the browser hands to JavaScript.
curl -s 'https://api.github.com/repos/curl/curl' -H 'Origin: https://h2check.org' -D - -o /dev/null | grep -i -E 'access-control-expose-headers|access-control-allow-origin|^vary'vary: Accept,Accept-Encoding, Accept, X-Requested-With access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, … access-control-allow-origin: *Without that list, client code reads only the seven safelisted headers:
Cache-Control,Content-Language,Content-Length,Content-Type,Expires,Last-Modified,Pragma. - Step 4.
Start
cors-allowlist.jswithnode cors-allowlist.js, then probe it with an origin on its list.curl -s 'http://localhost:8099/orders' -H 'Origin: https://app.example.com' -D - -o /dev/nullHTTP/1.1 200 OK Vary: Origin Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining X-Request-Id: req-42This is the shape to compare your API against: one exact origin,
Vary: Origin, credentials, and a named list of readable headers. - Step 5.
Send the same request from an origin that is not on the list, then stop the server.
curl -s 'http://localhost:8099/orders' -H 'Origin: https://evil.example' -D - -o /dev/nullHTTP/1.1 200 OK Vary: Origin X-Request-Id: req-42 X-RateLimit-Remaining: 59 Content-Type: application/jsonThe body still arrives over curl and the CORS headers are gone, which is what makes the browser discard it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| access-control-allow-origin holds your exact origin | The origin is on a list | Nothing. Record the list in the test notes. |
| The header changes to match every origin you send | The server reflects instead of matching | Ask for a list. With credentials on, raise it as a security defect. |
| access-control-allow-origin: * with access-control-allow-credentials: true | An invalid pair | The browser drops the response. Pick one of the two. |
| A per-origin header with no vary: origin | A CDN can serve one origin's header to another | Add Vary: Origin before the response is cached. |
| No access-control-expose-headers | JavaScript sees only the seven safelisted headers | Name the headers the client reads, such as a request id or a quota counter. |
| access-control-max-age missing or tiny | Every call repeats the preflight | See How to test preflight request. |
Common mistakes
What to check next
- How to test preflight request: the OPTIONS call that decides whether the browser sends the real request at all.
- How to test CORS with curl: the short version of this audit for a single GET.
- How to check HTTP response headers with curl: the same response carries caching and security headers worth reading in one pass.
- How to test API authentication: run it whenever credentials are allowed on a cross-origin route.
- CORS checker: paste a URL and an origin to get the same header set without a terminal.
FAQ
How to check if CORS is enabled?
Send any request with an Origin header and count the access-control-allow-origin lines in the response. Zero means the route has no CORS layer. The status code stays 200 either way, so the header is the only signal.
How to check which origins an API allows?
You cannot list them from outside. Test the origins you care about one at a time, then add one origin nobody owns. If that one is accepted too, the API has no list and the answer is every origin.
How to check CORS policy in the browser?
Open DevTools, Network tab, select the failing request, then the Headers panel. Compare the Origin you sent with access-control-allow-origin. The Console tab names the rule that failed, such as credentials with a wildcard.
Can access-control-allow-origin hold several origins?
No. The header carries one origin or a star. Servers that need several keep a list, match the incoming Origin against it, and echo the single matching value, which is what step 4 shows.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: cors preflight response, allowed origins, credentials, methods
- API security review
- API testing checklist
- Website launch checklist
- All api checks checks
intermediate6 minpublished updated Maks Verny