Skip to content

cors · July 10, 2026 · 6 min read

CORS Misconfigurations: When Access-Control-Allow-Origin Goes Wrong

Most CORS 'fixes' quietly open your API to every website. What the headers mean, the dangerous patterns, and safe configs for Next.js and Express.

Most developers meet CORS as an error message blocking their frontend, and the internet’s most popular answer is “just allow everything.” That fix makes the error disappear — and can quietly hand any website on the internet the ability to read your API’s responses. Here is what CORS actually does, the patterns that go wrong, and configurations that are both working and safe.

CORS relaxes a protection — it isn’t one

Browsers enforce the same-origin policy: JavaScript running on site-a.com cannot read responses from site-b.com. That rule is what stops a random page you visit from reading your webmail or your banking session in another tab. CORS (Cross-Origin Resource Sharing) is the mechanism for poking deliberate holes in that rule — your API telling browsers, “this other origin may read my responses.”

Two things follow. First, every CORS header you send is a hole you chose to poke; “fixing the CORS error” really means deciding who may read your data. Second, CORS only speaks to browsers — it does nothing against curl or server-side scripts. Its entire job is protecting your users’ browser sessions, which is exactly what sloppy configurations give away.

What the headers actually mean

  • Access-Control-Allow-Origin — which origin may read this response. A single origin, or *for “anyone.”
  • Access-Control-Allow-Credentials — whether the browser may send cookies and auth on the cross-origin request and expose the response. trueraises the stakes: now it’s a logged-in user’s data.
  • Access-Control-Allow-Methods — which HTTP methods cross-origin callers may use. GET only, or DELETE too?
  • Access-Control-Allow-Headers — which request headers they may send (for example, Authorization).
  • Access-Control-Expose-Headers — which response headers cross-origin JavaScript may read. Listing sensitive ones widens the hole.

The patterns that go wrong

Reflecting whatever Origin arrives, with credentials.The worst common pattern. The server reads the request’s Origin header and echoes it back in Access-Control-Allow-Origin, adding Access-Control-Allow-Credentials: true. The result: any website a logged-in user visits can make their browser call your API with their session and read the response — profile data, tokens, whatever the endpoint returns. It usually starts life as a shortcut to support “multiple frontends.”

Wildcard plus credentials. Access-Control-Allow-Origin: *together with credentials. The spec forbids this exact pair and browsers refuse to honor it — which is precisely why developers “work around” it with the reflection pattern above. If this pair is on your API, the intent it encodes (“everyone, with cookies”) is the problem, and the working version of that intent is worse.

Trusting the null origin. Requests can legitimately arrive with Origin: null — from sandboxed iframes, some redirects, and local files. Adding null to an allowlist looks harmless in a config array and effectively reopens the door the allowlist was meant to close, because that origin is easy for untrusted content to produce.

Over-permissive methods and headers. Advertising PUT, DELETE, and PATCH to cross-origin callers when your integration only needs GET, or exposing sensitive response headers, grants capability nobody will miss until it is misused.

For balance: a plain * without credentials is not automatically wrong. It is correct for genuinely public, unauthenticated data — a font, a public status feed. It becomes wrong the moment the same endpoint serves anything user-specific.

Safe patterns

  • An explicit origin allowlist. Compare the incoming Origin against a fixed list of your own origins and echo it back only on an exact match. Send Vary: Originso caches don’t mix responses.
  • No credentials unless you need them. If your frontend authenticates with bearer tokens in headers rather than cookies, you may not need Allow-Credentials at all.
  • Least privilege everywhere else.Only the methods and headers your cross-origin clients actually use; expose no response headers you don’t have to.
  • No CORS headers at all on endpoints that are only ever called same-origin. Absence of CORS is the secure default.

How to check your API

Open DevTools → Network in your app, click an API request, and read the response headers. Then check what your API returns when the origin isn’t yours:

curl -s -D - -o /dev/null -H "Origin: https://example.org" https://api.yoursite.com/me

If the response echoes https://example.org back in Access-Control-Allow-Origin — especially alongside Access-Control-Allow-Credentials: true — your policy trusts strangers. Inspecting your own headers like this is standard, non-destructive verification.

Fixing it in your framework

Next.js — handle it in middleware (or per route handler) with an allowlist:

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

const ALLOWED_ORIGINS = ["https://app.yoursite.com", "https://www.yoursite.com"];

export function middleware(request: NextRequest): NextResponse {
  const origin = request.headers.get("origin") ?? "";
  const response = NextResponse.next();

  if (ALLOWED_ORIGINS.includes(origin)) {
    response.headers.set("Access-Control-Allow-Origin", origin);
    response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    response.headers.set("Vary", "Origin");
  }

  return response;
}

export const config = { matcher: "/api/:path*" };

Express — the cors package accepts an array; use it instead of origin: true (which reflects) or "*":

import cors from "cors";

app.use(
  cors({
    origin: ["https://app.yoursite.com"],
    credentials: true,
    methods: ["GET", "POST"],
  })
);

If an AI assistant wrote your backend, search the code for origin: "*" and origin: true — servers generated with Cursor, Bolt, or v0 very often ship one of the two, because that is what made the error disappear during development. Our vibe-coding security guide covers the rest of that gap.

CORS also interacts with the rest of your response headers: SameSite decides when cookies ride along cross-site in the first place (see cookie security flags), and the broader header story is in our security headers guide.

How BoringSec checks this

BoringSec’s CORS scanner — one of 24 runtime scanner modules — actively tests your endpoints’ live policy instead of guessing from config: whether arbitrary origins get reflected back with credentials allowed (a high-severity finding), wildcard-with-credentials, trust of the null origin, dangerous methods, and sensitive response headers exposed. Every finding records the exact request and response as evidence and cites its standard, in line with our evidence-first rule: critical and high findings without captured proof are automatically downgraded to medium. The scoring model is documented in our methodology.

FAQ

Is Access-Control-Allow-Origin: * always insecure?

No. For truly public, unauthenticated data it is correct. It becomes a problem when the endpoint returns user-specific data or is combined with credentials.

Why do browsers block wildcard-with-credentials but reflection works?

The spec forbids the literal *+ credentials pair, but echoing each caller’s specific origin satisfies the letter of the rule while granting every origin the same access. That is why reflection with credentials is the pattern rated high severity.

Does CORS protect my API from bots and scrapers?

No. CORS is enforced by browsers only; server-side clients ignore it entirely. It protects your users’ browser sessions, not your server — you still need authentication and rate limiting.

Keep reading

Check your site now

One scan applies the relevant checks from 20 base URL modules plus 3 verified-owner background engines. Free, no signup, with base results appearing first.

Scan your site free