How to Fix CORS Misconfigurations in Next.js
CORS errors are one of the most common problems developers hit when building web apps. The usual fix? Copy a snippet from Stack Overflow that sets Access-Control-Allow-Origin: * and move on. It works, the error goes away, and you never think about it again.
The problem is that this "fix" often introduces a serious security vulnerability. CORS exists to protect your users, and misconfiguring it can let any website in the world make authenticated requests to your API on behalf of your logged-in users.
Here's what CORS actually does, the three misconfigurations I see most often, and how to set it up properly in Next.js.
What CORS actually does
CORS stands for Cross-Origin Resource Sharing. It's a browser mechanism that controls which websites can make requests to your API.
By default, browsers block requests from one origin (say, evil-site.com) to another origin (your API at api.yourapp.com). This is called the Same-Origin Policy, and it's one of the most important security features in the browser. CORS is the system that selectively relaxes this restriction when you explicitly allow it.
When a browser makes a cross-origin request, it sends an Origin header. Your server responds with Access-Control-Allow-Origin to tell the browser whether that origin is allowed. If the response header doesn't match the request origin, the browser blocks the response.
The key point: CORS is enforced by the browser, not your server. Your server still processes the request either way. CORS just controls whether the browser lets the calling website read the response.
Misconfiguration 1: Wildcard with credentials
This is the most dangerous one. It looks like this:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
The intent is "let anyone access my API and send cookies." Fortunately, browsers actually block this specific combination. The spec forbids it. But developers often work around it by reflecting the origin instead (see misconfiguration 2), which is even worse.
If you're setting Access-Control-Allow-Origin: *, make sure you're not also setting Access-Control-Allow-Credentials: true. If you need credentials, you need to specify exact origins.
Misconfiguration 2: Reflecting the origin
This is the one that actually causes damage. Instead of a wildcard, the server reads the Origin header from the request and echoes it back in the response:
// DANGEROUS: Do not do this
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
This effectively says "every website in the world is allowed to make credentialed requests to my API." An attacker can host a page that makes fetch requests to your API, and the browser will send your users' cookies along with those requests. The attacker can then read the responses.
This means any website can read your users' data, trigger actions on their behalf, or exfiltrate sensitive information. All the user has to do is visit the attacker's page while logged into your app.
Misconfiguration 3: Allowing null origin
Some CORS implementations include null in the allowed origins list:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
The null origin is sent by requests from local files, redirects, and sandboxed iframes. An attacker can craft a sandboxed iframe that sends requests with a null origin, bypassing your CORS policy entirely. Never allow null as an origin.
How to configure CORS properly in Next.js
API Routes (App Router)
For Next.js API routes using the App Router, handle CORS explicitly in your route handlers:
// app/api/example/route.ts
const ALLOWED_ORIGINS = [
'https://yourapp.com',
'https://www.yourapp.com',
];
function getCorsHeaders(request: Request) {
const origin = request.headers.get('origin');
const isAllowed = origin && ALLOWED_ORIGINS.includes(origin);
return {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
};
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: getCorsHeaders(request),
});
}
export async function GET(request: Request) {
const headers = getCorsHeaders(request);
// Your logic here
return Response.json({ data: 'example' }, { headers });
}
The critical part is the ALLOWED_ORIGINS array. Only origins you explicitly list will be allowed. Everything else gets an empty Access-Control-Allow-Origin header, which tells the browser to block the response.
Middleware approach
If you have many API routes, handling CORS in middleware is cleaner:
// middleware.ts
const ALLOWED_ORIGINS = [
'https://yourapp.com',
'https://www.yourapp.com',
];
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin');
const isAllowed = origin && ALLOWED_ORIGINS.includes(origin);
// Handle preflight
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
},
});
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}
export const config = {
matcher: '/api/:path*',
};
Environment-based configuration
In practice, you'll want different allowed origins for development and production:
const ALLOWED_ORIGINS = process.env.NODE_ENV === 'development'
? ['http://localhost:3000', 'http://localhost:3001']
: ['https://yourapp.com', 'https://www.yourapp.com'];
Never leave localhost in your production allowed origins list. It's a common mistake that developers make when rushing to deploy.
Quick checklist
Before you ship, verify these five things about your CORS configuration:
Never use Access-Control-Allow-Origin: * with credentials. The browser blocks it, but if your code tries to work around it by reflecting the origin, you've introduced a real vulnerability.
Never reflect the request origin without validation. Always check against an explicit allowlist of origins you control.
Never allow null as an origin. It's exploitable via sandboxed iframes.
Set Access-Control-Max-Age to cache preflight responses. 86400 (24 hours) reduces the number of OPTIONS requests and improves performance.
Use environment variables for allowed origins. Don't hardcode localhost in production.
How to check yours
Open your browser's dev tools, go to the Network tab, and look at any request to your API. Check the Access-Control-Allow-Origin response header. If it says * or if it mirrors whatever origin sent the request, you have a problem.
You can also run a quick scan with Hexora at hexora.uk, which checks for all three CORS misconfigurations automatically and shows you exactly what the server responded with.
The bottom line
CORS misconfigurations are dangerous because they're invisible. Your app works perfectly. Your users don't notice anything. But any website can silently make requests to your API using your users' sessions. The fix is straightforward: maintain an explicit allowlist of origins and never reflect or wildcard with credentials.
Five minutes of configuration. Every user protected.
Worried about your own site's security? Get a free scan in seconds.
Scan your site for free