Choose your stack
Fix CORS misconfiguration in Node.js and Express
Use the cors middleware with an origin function that checks an exact allowlist of full origins from configuration, and credentials: true.
Open the fix card Python · FlaskFix CORS misconfiguration in Python and Flask
Register flask-cors for the API routes with exact origins from configuration and supports_credentials=True.
Open the fix card Java · Spring BootFix CORS misconfiguration in Java and Spring Boot
Declare a CorsConfigurationSource bean whose CorsConfiguration lists the front ends' exact origins from configuration with setAllowedOrigins and sets setAllowCredentials(true), and apply it with CorsFilter or http.cors().
Open the fix card C# · ASP.NET CoreFix CORS misconfiguration in C# and ASP.NET Core
Add a named CORS policy with WithOrigins(allowedOrigins) from configuration and AllowCredentials(), call app.UseCors(), and require the policy on the API endpoints.
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix CORS misconfiguration in Node.js and Express, in the same language.
- Python and FastAPI: no card yet. The nearest is Fix CORS misconfiguration in Python and Flask, in the same language.
- Python and Django: no card yet. The nearest is Fix CORS misconfiguration in Python and Flask, in the same language.
- Go and net/http: no card yet. The nearest is Fix CORS misconfiguration in Node.js and Express.
- Ruby on Rails: no card yet. The nearest is Fix CORS misconfiguration in Node.js and Express.
- PHP and Laravel: no card yet. The nearest is Fix CORS misconfiguration in Node.js and Express.
How it happens
- With fetch(url, { credentials: "include" }) the browser attaches the user's cookies for the API to a cross-origin request. The response decides whether the calling page may read it: Access-Control-Allow-Origin must equal the page's origin exactly and Access-Control-Allow-Credentials must be true. An API that echoes any Origin with credentials lets every site the user visits read the user's data through the user's own browser.
- Browsers refuse Access-Control-Allow-Origin: * on credentialed requests, and three of the four frameworks refuse to configure it: Spring throws IllegalArgumentException on the first CORS request, ASP.NET Core throws InvalidOperationException at startup, and flask-cors raises ValueError when send_wildcard is set, while the cors package sends the header and leaves the browser to refuse it. The usual workaround is reflection: cors({ origin: true }), allowedOriginPatterns("*"), SetIsOriginAllowed(_ => true), or flask-cors's default origins="*" with supports_credentials=True, which echoes the Origin without complaint.
- Pattern checks meant to cover all of a company's sites accept look-alikes. A substring test or an unanchored regular expression accepts https://portal.example.test.evil.example, where the trusted name is only the start of the attacker's domain; an ends-with test accepts https://evil-example.test, a domain anyone could register; an unescaped dot in portal.example.test also matches portal-example.test. The cors package's README illustrates its RegExp option with /example\.com$/, which also matches evil-example.com.
- Allowlisting null, often added for local file: development, admits sandboxed iframes and data: URLs, which any page can create. Checks that ignore the scheme admit http:// origins, where anyone on the network path can inject script. Trusting every subdomain turns one XSS or dangling DNS record on any of them into access to the API.
- CORS governs reading, not sending. A cross-origin GET, or a POST with a form or text/plain body, reaches the server without a preflight, so state-changing endpoints still need CSRF defences; a JSON PUT like this card's needs a preflight first, which an exact allowlist refuses and a reflecting one approves.
- Whether the cookie is attached at all depends on the browser and the cookie: a SameSite=Lax cookie stays home on cross-site fetches but is sent from sibling subdomains, and a SameSite=None cookie, which an API needs to serve a front end on another site, is sent from any site in Chrome, which does not block third-party cookies by default, while Firefox and Safari restrict them by default. The same bug can be exploitable in one browser and not another, which is why the allowlist must not depend on it.
Fix principles
- Use the framework's CORS support with an explicit list of full origins compared as exact strings: cors with an origin function in Express, flask-cors with literal origins, CorsConfiguration.setAllowedOrigins in Spring, WithOrigins in ASP.NET Core. Keep the list in configuration, per environment, with no wildcard fallback.
- Never reflect the Origin header without that check, and keep pattern hooks (a RegExp or regex string, allowedOriginPatterns, SetIsOriginAllowed, a contains or ends-with test) away from credentialed endpoints. If every subdomain really is trusted, use the framework's subdomain matcher, allowedOriginPatterns("https://*.example.test") or SetIsOriginAllowedToAllowWildcardSubdomains(), which pins the scheme and requires the dot.
- Send Access-Control-Allow-Credentials only with an allowed origin, and Vary: Origin on every response whose CORS headers depend on the Origin, refusals and requests without Origin included: the Fetch standard's note on CORS and HTTP caches asks for it, because a cache that stores a response without it can replay a refusal to a listed front end, or one origin's grant to another. The frameworks add Vary when they echo an allowed origin (ASP.NET Core and flask-cors only when more than one origin is listed), and only Spring adds it to refusals, so the Express, Flask and ASP.NET Core cards add it themselves. Mark per-user responses Cache-Control: no-store or private so that no shared cache serves them across origins or users.
- Never allowlist null, http:// origins or origins you do not control, and keep development origins such as http://localhost:5173 out of production configuration.
- Remember what CORS does not do: it does not authenticate or authorize requests and does not stop them being sent. Keep authorization on the server, keep CSRF protection on cookie-authenticated writes, and require a JSON content type or a custom header on endpoints that rely on the preflight.
The scenario
The API at https://api.example.test returns the signed-in user's invoices (GET /api/invoices) and changes their email (PUT /api/account/email with a JSON body). Two browser front ends call it with fetch(..., { credentials: "include" }): https://portal.example.test and https://admin.example.test, the only entries on the allowlist, which every stack reads from configuration. Requests carry the session cookie sid; the fixture has one user, Alice. Every stack runs the same thirteen tests through a small browser model that applies the Fetch standard's CORS check and preflight rules, so a test fails exactly when a browser would let the calling page read the response or send the update.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
- The exploit tests send what a page on another origin makes the browser send, with Alice's cookie: an arbitrary origin, Origin: null, https://evil-example.test, https://portal.example.test.evil.example, https://portal-example.test, http://portal.example.test and the sibling https://forum.example.test, plus a credentialed JSON PUT that needs a preflight. The browser model applies the Fetch standard's CORS check (exact Access-Control-Allow-Origin and Access-Control-Allow-Credentials: true) and preflight rules, and each test fails if the page could read the invoices or the PUT would be sent. Functional tests keep both listed origins, the preflighted PUT and requests without Origin working, and require Vary: Origin on every response: those that echo an origin, a refused origin's and one to a request without Origin.
- Static analysis barely covers framework CORS configuration. No Semgrep rule found anything on this card in any stack: Semgrep's flask-cors rule matches only an explicit origins="*", and its Express and Java rules only headers set by hand. CodeQL's js/cors-permissive-configuration models the cors package and should catch origin: true but not a RegExp, Sonar's S5122 caught AllowAnyOrigin() but not SetIsOriginAllowed, and nothing checked here models flask-cors or Spring's CorsConfiguration. Each card's scannerRules records what was run and what it found.
Identifiers
CWE-942 (permissive cross-domain policy with untrusted domains) describes the vulnerable and wrong implementations, whose CORS policy trusts origins it should not; OWASP Top 10:2025 maps it to A02 Security Misconfiguration. CWE-346 (origin validation error) describes the flawed origin check itself, and the 2025 list maps it to A07 Authentication Failures; Sonar's S5122 cites both. CAPEC is left empty on purpose: MITRE lists no attack pattern for CWE-942, and the patterns it lists for CWE-346, such as JSON hijacking and cache poisoning, describe different attacks. ASVS 5.0.0 requirement 3.4.2 asks for an allowlist when Access-Control-Allow-Origin is derived from the Origin header, and 3.5.2 applies to the card's PUT endpoint, which relies on the preflight.
References
- WHATWG Fetch Standard: CORS protocol, CORS check, credentials and HTTP caches
- MDN: Cross-Origin Resource Sharing (CORS)
- OWASP HTML5 Security Cheat Sheet: Cross Origin Resource Sharing
- OWASP Web Security Testing Guide: Testing Cross Origin Resource Sharing
- OWASP Top 10:2025 A02 Security Misconfiguration
- OWASP Top 10:2025 A07 Authentication Failures
- CWE-942: Permissive Cross-domain Security Policy with Untrusted Domains
- CWE-346: Origin Validation Error
- OWASP ASVS 5.0.0, V3 Web Frontend Security (3.4.2, 3.5.2)
- PortSwigger Web Security Academy: CORS
- James Kettle: Exploiting CORS misconfigurations for Bitcoins and bounties
- MDN: Third-party cookies (browser defaults)
Practice in the browser
- CORS policy lab: Grant CORS to the right origins only · Foundation, JavaScript
Guides on this weakness
Configure CORS and CSP around the browser capabilities you need
Control who can read API responses and which resources a page may execute without confusing either control with authorization.
Render rich text without granting it application privileges
Choose text rendering or a restricted HTML policy, then test the final DOM after every transformation.
Constrain desktop IPC and privileged helpers
Keep renderer content, local callers and plugins from turning a narrow feature into unrestricted operating-system access.
OAuth 2.0 and OpenID Connect login: authorization code with PKCE
Implement OAuth 2.0 and OpenID Connect login with the authorization code flow and PKCE, keep each transaction bound to its browser, and block redirect, mix-up, code-injection and token-theft attacks.
Prevent DOM XSS, postMessage abuse and client-side injection
Trace attacker-controlled values from the URL, messages and storage to the browser APIs that parse HTML or run code, then close those sinks with safe APIs, a sanitizer, Trusted Types and strict message checks.
Defend OAuth and OIDC against redirect and token attacks
Apply RFC 9700 and RFC 10017: exact redirect matching, PKCE with downgrade protection, state and iss binding, refresh-token rotation, sender-constrained tokens, and a backend-for-frontend so browser tokens cannot be stolen.