Access-Control-Allow-Origin: * — the CORS wildcard that quietly leaks your Next.js API
You're calling your own API from your own frontend, the browser blocks it, and the console screams about a missing Access-Control-Allow-Origin header. So you do the thing every Stack Overflow answer suggests: set the header to *. The error vanishes instantly. And you've just told every website on the internet that it's allowed to read your API's responses.
That one character — * — is a wildcard. It means "any origin." On a public, read-only endpoint that's sometimes fine. On a route that returns user data, or one that trusts a cookie, it's a quiet data leak waiting for someone to notice.
What CORS actually does
The browser enforces the same-origin policy: by default, JavaScript running on evil.com cannot read responses from yourapp.com. CORS is the controlled exception. When yourapp.com responds with Access-Control-Allow-Origin: https://yourapp.com, the browser decides this origin is allowed to read this response and hands the data to the script.
The key word is read. CORS is not about who can send a request — anyone can always send one. It governs who is allowed to read the response. So when you set:
Access-Control-Allow-Origin: *
you're telling the browser to hand your API's responses to script running on any page the user happens to have open. That's the whole game.
Why the wildcard is dangerous
Here's the canonical mistake — a Next.js route handler that returns account data and slaps a wildcard on it to silence the CORS error:
import { NextResponse } from "next/server";
export async function GET() {
const data = await getAccountSummary();
return NextResponse.json(data, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
},
});
}
- Warningapp/api/account/route.ts:7
Permissive CORS configuration
Reflect a specific allowlist of trusted origins instead of '*', especially on routes that read cookies or Authorization headers.
Two things make this bite:
1. Any site can now read this endpoint. If the route is reachable without credentials and returns anything sensitive — internal IDs, email addresses, usage stats, a customer list — a script on any domain can fetch() it and read the JSON. You've effectively published the endpoint to the entire web.
2. The credentials line is the real tell. Notice Access-Control-Allow-Credentials: true. That asks the browser to include the user's cookies on cross-origin requests, then expose the response to the calling script. Pairing it with a wildcard origin is the most dangerous configuration in CORS: "any site may make authenticated requests as the logged-in user and read what comes back." That's a textbook path to cross-site data theft.
The saving grace is that the spec forbids exactly this pair. When the origin is *, the browser simply ignores Allow-Credentials: true — and a credentialed cross-origin request gets its response withheld from the script. So the code above doesn't even work the way the author hoped. The usual "fix" is to make it work by reflecting the request's Origin header back unconditionally, which satisfies the browser and removes the only protection left. Now you genuinely have credentialed cross-origin reads from anywhere. The wildcard is the smell that leads people straight to that hole.
The fix: an explicit allowlist
Don't send *, and don't reflect whatever Origin arrives. Keep a small list of origins you actually trust and echo back only those.
import { NextResponse } from "next/server";
const ALLOWED = new Set([
"https://yourapp.com",
"https://app.yourapp.com",
]);
export async function GET(request: Request) {
const origin = request.headers.get("origin") ?? "";
const data = await getAccountSummary();
const res = NextResponse.json(data);
if (ALLOWED.has(origin)) {
res.headers.set("Access-Control-Allow-Origin", origin);
res.headers.set("Access-Control-Allow-Credentials", "true");
res.headers.set("Vary", "Origin");
}
return res;
}
Three details that matter:
- Reflect from an allowlist, never blindly.
ALLOWED.has(origin)is the whole point. Reflecting the rawOriginheader with no check is functionally identical to*, just harder to spot in review. - Set
Vary: Origin. The response now differs per origin, so caches (CDN, browser) must key onOrigin. Skip this and a CDN can serve one origin's allowed response to a different origin. - Handle the preflight. For anything beyond a simple
GET, browsers send anOPTIONSpreflight first. Add anOPTIONShandler (or middleware) that returns the same allowlist-checked headers plusAccess-Control-Allow-MethodsandAccess-Control-Allow-Headers.
If your endpoint is genuinely public and credential-free — a status page, public price feed, open data — then Access-Control-Allow-Origin: * is acceptable as long as you never send Allow-Credentials: true and the response contains nothing tied to a user. The wildcard is a deliberate decision for public data, not a default you reach for to kill an error.
A 30-second self-check
# 1. Any wildcard CORS header in your codebase?
grep -rn "Access-Control-Allow-Origin" . --include=*.ts --include=*.js
# 2. The dangerous pair — wildcard plus credentials in the same file?
grep -rln "Access-Control-Allow-Origin" . | xargs grep -l "Allow-Credentials"
# 3. Blind reflection (just as bad as *)?
grep -rn "Allow-Origin.*headers.get" . --include=*.ts
Any hit on a route that returns user-scoped data, or that pairs a wildcard with credentials, is a finding. Reflecting the request origin without an allowlist check is the same bug wearing a disguise.
FAQ
Is Access-Control-Allow-Origin: * always wrong?
No. For truly public, credential-free endpoints it's fine and intentional. It becomes a vulnerability when the response is user-specific or when you also send credentials. The risk is using it as a reflex to make CORS errors go away.
Doesn't the browser block wildcard + credentials anyway?
Yes — when the origin is * the browser ignores Allow-Credentials and won't expose a credentialed response. The danger is the workaround it pushes you toward: reflecting the Origin header unconditionally, which does allow credentialed cross-origin reads from anywhere.
My API uses an auth token in a header, not cookies. Am I safe?
Safer, but not off the hook. If a malicious cross-origin script can get hold of that token and attach it (or you set Allow-Credentials), the same exposure applies. Use an explicit origin allowlist regardless.
Where should I set CORS in Next.js?
Per-route in the handler for fine control, or centrally in middleware.ts / next.config.js headers() for app-wide policy. Wherever it lives, the value should come from an allowlist — never a hardcoded * on authenticated routes.
GuardLayer flags wildcard CORS (and 19 other Next.js + Supabase issues) on every push, and shows you the allowlist fix inline before it ships.
Catch this before it ships — free
GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.