CORS (Cross-Origin Resource Sharing) is the browser mechanism that lets a page make requests to an origin other than its own when the destination server explicitly authorises it through HTTP headers. It relaxes the Same-Origin Policy (SOP) under server control. W3C defined it in 2014 (now part of WHATWG's Fetch Standard) and every modern browser implements it. When misconfigured, it becomes one of the most frequent sources of web vulnerabilities.
This guide explains what problem CORS solves, how it works internally (simple request vs preflight), the headers that matter, the seven misconfigurations appearing again and again in web application and API pentesting, documented real cases, secure configuration by scenario and how proper hardening gets tested in an audit.
What problem CORS solves
To understand CORS you first have to understand the Same-Origin Policy (SOP): browser rule since Netscape 2 (1995) that prevents JavaScript code loaded from one origin from reading data from a different origin. Origin gets defined by the triple protocol + domain + port. https://app.secra.es and https://api.secra.es are different origins; http://secra.es:80 and https://secra.es:443 too.
Without SOP, a tab open on evil.example.com could read your active session in mybank.com through a JavaScript fetch. SOP exists to prevent it.
The problem is many legitimate applications need exactly that: the SPA frontend on app.secra.es consumes the API on api.secra.es. SOP would block the call by default. CORS is the standard way for the server to declare "I accept app.secra.es calling me and reading my response".
How CORS works
The protocol distinguishes two cases by request nature.
Simple Request
A request is "simple" if it meets the three conditions:
- Method GET, HEAD or POST.
- Headers only within the CORS-safelisted list (Accept, Accept-Language, Content-Language, Content-Type limited to
application/x-www-form-urlencoded,multipart/form-dataortext/plain). - No custom readable streams and no upload event listeners.
In this case, the browser sends the request directly, including the header Origin: https://app.secra.es. The server responds with the normal response plus the header Access-Control-Allow-Origin: https://app.secra.es (or other values we'll see below). If the browser doesn't see a valid header, it drops the response and the JavaScript sees an error.
Important: the request gets sent and executed on the server even if CORS rejects it afterwards. CORS doesn't protect the server from side-effects, only stops the browser from delivering the response to the origin JavaScript.
Preflight
When the request isn't simple (PUT, DELETE, custom headers, JSON content-type), the browser first sends an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers headers. The server responds with Access-Control-Allow-Methods and Access-Control-Allow-Headers declaring what it accepts. If the response authorises, the browser sends the real request; if not, it gets aborted without executing.
The preflight is the difference between "the browser prevents the leak" and "the browser prevents even the side-effect execution". That's why Content-Type: application/json triggers preflight: any destructive call passes through prior validation.
Headers that matter
Essential list for audit:
Access-Control-Allow-Origin. Authorised origin. Can be a specific domain (https://app.secra.es),*(anyone, careful) ornull. Only one value; no comma-separated domain list allowed.Access-Control-Allow-Credentials.trueallows sending cookies and authentication headers with the request. Critical when there's a session.Access-Control-Allow-Methods. Authorised HTTP methods (GET, POST, PUT, DELETE, PATCH).Access-Control-Allow-Headers. Custom headers the client can send (Authorization, X-Requested-With, etc.).Access-Control-Expose-Headers. Response headers the client JavaScript can access. By default it only sees CORS-safelisted ones; if the API returns a token inX-Custom-Tokenand you want the JS to read it, you have to expose it.Access-Control-Max-Age. Time in seconds the browser can cache the preflight. Reduces overhead.
The browser respects these headers only if it receives them from the server; the client JavaScript can't fake them for itself.
Exploitable misconfigurations
The seven patterns appearing practically in any professional web audit.
1. Allow-Origin wildcard with Allow-Credentials true
The combination is invalid per specification and current browsers reject it, but tolerant implementations existed and dangerous variants still appear.
Most frequent today: the server reflects the client's Origin header as the value of Access-Control-Allow-Origin and sends Allow-Credentials: true. Functionally equivalent to allowing anyone with cookies. Any site the user visits can read the authenticated API response.
Detection: send a request with Origin: https://attacker.com. If the response returns Access-Control-Allow-Origin: https://attacker.com, vulnerable.
2. Origin reflection without allowlist
Naive backend implementation: headers["Access-Control-Allow-Origin"] = request.headers["Origin"]. Passes any domain including the attacker's.
Variant with badly-done allowlist: if "secra.es" in origin: allows https://attacker-secra.es.evil.com or https://secra.es.evil.com. Badly-thought startsWith/endsWith opens the door.
3. Trust in subdomains without DNS control
https://*.secra.es looks reasonable until an old domain (forgotten.secra.es) has a CNAME pointing to a lapsed third-party service. The attacker claims the subdomain, stands up a site on it and their requests meet the CORS rule.
Also applies to "any subdomain of friendly-company.com": just one XSS vulnerability on any whitelisted subdomain is enough to escalate to the protected API.
4. null allowed
Some backends include null in the allowlist by mistake. null origin gets sent when the request comes from:
- A local file loaded in the browser.
- Sandboxed iframe without
allow-same-originattribute. - Data URI document.
An attacker can force null origin with a controlled sandbox iframe and read the authenticated response.
5. CORS only on one path
/api/v1/users has strict CORS but /api/v1/users/avatar doesn't. The attacker finds the forgotten endpoint and exploits it. Typically appears when endpoints get added without passing through the central CORS middleware.
6. CORS headers in error responses
The backend responds with CORS headers only on 200 OK. The 404 error page has no CORS headers but the body returns sensitive information. Some frameworks have this bug by default.
7. Preflight cached too long with Allow-Headers wildcard
Access-Control-Allow-Headers: * with Max-Age: 86400 lets the browser cache 24 hours of any header authorisation. If the developer tightens the policy later, clients with valid cache keep being able to send headers that shouldn't be allowed anymore.
Real exploitation cases
Misconfigured CORS opens concrete scenarios by context.
Stealing private data from an authenticated API. The API trusts session cookies, the vulnerable origin reflects Origin. The attacker hosts JavaScript on their site that does fetch(api_endpoint, { credentials: "include" }). The logged-in user's browser sends the cookies, the API responds, the attacker receives the data. No XSS involved.
Escalating limited XSS to account takeover. An XSS on a minor subdomain translates into compromise of the whole API if the CORS rule trusts *.secra.es.
Bypassing weak anti-CSRF defences. Applications trusting CORS as sole anti-CSRF protection instead of dedicated tokens. With broken CORS, the defence falls.
Stealing JWT tokens stored in localStorage. If the API exposes an endpoint that returns sensitive data including tokens, and CORS is broken, the attacker can read the user's JWT without attacking the endpoint itself.
Information disclosure in error responses. Endpoints returning error messages with sensitive data (table names, paths, versions) and open CORS expose reconnaissance.
A public documented case: the Bitwarden bug bounty in 2018 revealed that vault.bitwarden.com accepted null as origin, allowing an external site to read authenticated responses through sandbox iframes. Reported by Tavis Ormandy, fixed in hours.
Secure configuration by scenario
The patterns an experienced web audit team applies.
Public API without authentication
No session or cookies. Acceptable Allow-Origin: * without Allow-Credentials. Suitable for public data APIs (prices, public stock, administrative open data).
API with header authentication (Bearer token)
The client JS sends Authorization: Bearer <jwt>. CORS needs to authorise the header (Allow-Headers: Authorization) and the frontend origin (Allow-Origin: https://app.secra.es). No need for Allow-Credentials because there are no cookies. The typical pattern of modern SPA.
API with cookie authentication
Here you can't use wildcards. The policy has to be:
Access-Control-Allow-Origin: https://app.secra.es(specific origin)Access-Control-Allow-Credentials: trueAccess-Control-Allow-Methods: GET, POST, PUT, DELETEAccess-Control-Allow-Headers: Content-Type, X-CSRF-Token
And combine with dedicated CSRF tokens, not trust CORS alone.
Multi-tenant with many origins
Explicit allowlist in backend (not regex), strict Origin validation, return the specific domain from the list, don't reflect.
const allowedOrigins = [
"https://app.secra.es",
"https://admin.secra.es",
"https://trusted-partner.com"
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
The Vary: Origin header is critical: without it, CDN caches can serve the response authorised for app.secra.es also to another origin, nullifying the protection.
Subdomains with strict DNS
If you want to accept *.secra.es, keep an updated inventory, retire unused subdomains, watch the organisation's TLS certificates with crt.sh or Censys monitoring and avoid CNAMEs toward external services without clear ownership.
How it gets tested in an audit
A web app pentester systematically executes:
- Origin map. Enumerate every API endpoint the frontend consumes.
- Origin injection. For each endpoint, replay the request with several
Originvalues: frontend domain, attacker.com, null, https://app.secra.es.evil.com, https://evil.app.secra.es. - Response analysis. Note which
Access-Control-Allow-Originvalue each case returns, whether it sendsAllow-Credentials: true, whether there'sVary: Origin. - Preflight test. For each endpoint with a non-simple verb, check the OPTIONS and the allowed headers.
- Regex test. Try variants that break badly-written allowlists: prefixes, suffixes, special characters.
- Exploitable PoC. For any confirmed finding, build an HTML PoC demonstrating the read of authenticated response from external origin. Deliver as reproducible evidence.
Common tools: Burp Suite with extensions (CORS, Param Miner), CORScanner, ffuf with custom headers, custom Python scripts.
Operational details in the web application pentesting guide.
Compliance fit
Misconfigured CORS materialises risks covered by frameworks:
- NIS2 (article 21). Technical risk management measures. An API exposed via open CORS breaches the duty of reasonable protection.
- DORA (article 9). Digital operational resilience of financial services. APIs are critical ICT.
- ISO 27001:2022 (control 8.26). Application security requirements.
- PCI DSS v4.0 (req. 6.4). Web application protection. CORS gets included in payment application audits.
- GDPR. A CORS vulnerability leaking personal data constitutes a notifiable breach.
Frequently asked questions
Does CORS protect the server or the user?
The user, through the browser. The server still executes the request; CORS only prevents the JavaScript of the unauthorised origin from reading the response. That's why CORS isn't defence against side-effects nor a substitute for CSRF tokens, authentication or authorisation.
Does CORS apply to requests from curl or Postman?
No. CORS is enforced by the browser. A request from curl, Postman, a server script or any non-browser client reaches the backend and executes without CORS restrictions. That's why the API must have independent auth, authorisation, rate limiting and input validation.
Is using Allow-Origin: * safe?
Only if the API requires no authentication and the data is truly public. For any endpoint with session, cookies or authorisation token, the wildcard is a vulnerability.
Difference between CORS and CSP?
CORS controls which origins can read cross-origin resources (relaxes SOP). Content-Security-Policy controls which origins can load resources into a page (restricts what the browser executes). They're complementary, not substitutes.
What about WebSockets, fetch streaming, EventSource?
WebSockets don't use CORS but their own handshake with an Origin header the server validates manually. EventSource does respect CORS. Fetch streaming follows standard CORS rules plus stream-specific restrictions.
How do I configure CORS in Next.js, Express or FastAPI?
- Next.js App Router: middleware or
headers()innext.config.jsfor API routes; orResponsewith explicit headers in route handlers. - Express:
corspackage with explicit allowlist. - FastAPI:
CORSMiddlewaremiddleware withallow_originslist. - Spring Boot:
@CrossOriginannotation with specificoriginsor global configuration viaWebMvcConfigurer.
In every case, avoid automatic reflection of the Origin header and validate against a closed list.
Does CORS need auditing on every release?
At minimum on every architecture change: adding a new subdomain, integrating a third-party provider, refactoring security middleware, separating microservices. And in every periodic external audit as a standard part.
Related resources
- Web application pentesting: where CORS audit fits as a standard sub-task.
- API pentesting REST and GraphQL: API endpoints are the scenario most affected by CORS errors.
- What is JWT and token security: JWT tokens are the main victim of open CORS in modern SPAs.
- What is a WAF: network-layer control that mitigates some CORS errors' impact but doesn't replace them.
CORS audit at Secra
At Secra we review CORS as a standard part of any web or API audit: endpoint enumeration, systematic allowlist testing with adversarial origins, preflight validation, header check on error responses, Vary: Origin and CDN caching verification, and a deliverable with reproducible PoC for each finding. If your team is about to deploy a new API, is migrating to modern SPA or has never audited the frontend CORS policy, get in touch via contact or check our web and mobile audit service.
About the author
Secra Solutions team
Ethical hackers with OSCP, OSEP, OSWE, CRTO, CRTL and CARTE certifications, 7+ years of experience in offensive cybersecurity, and authors of CVE-2025-40652 and CVE-2023-3512.