Web Security

CORS Misconfiguration: How It Happens and How to Fix It

CORS misconfigurations let attackers steal data from authenticated users. Learn how wildcard origins, null origins, and regex mistakes create vulnerabilities — and how to configure CORS correctly.

October 21, 20255 min readShipSafer Team

CORS (Cross-Origin Resource Sharing) misconfigurations are a common vulnerability in APIs and web applications. A misconfigured CORS policy can allow an attacker's website to make authenticated requests to your API and read the responses — effectively stealing any data your logged-in users can access.

How CORS Works

By default, browsers block cross-origin requests made by JavaScript. This is the Same-Origin Policy: a script at app.com can't fetch data from api.com.

CORS is a browser mechanism that lets servers relax this restriction for specific origins. When a server responds with:

Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Credentials: true

...the browser allows the app.com JavaScript to read the response from api.com.

The key insight: CORS is enforced by the browser, not the server. The request still reaches your server — the browser just decides whether to give the response to the requesting JavaScript.

Dangerous CORS Patterns

1. Wildcard origin with credentials

The most obviously broken configuration:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Browsers actually block this combination — they won't send credentials (cookies, auth headers) with wildcard origin requests. But many developers then "fix" this by dynamically reflecting the Origin header:

// ❌ Dangerous: reflects any origin
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});

This is equivalent to Access-Control-Allow-Origin: * with credentials enabled — any website can make authenticated requests to your API.

Attack scenario:

  1. Attacker hosts evil.com with JavaScript that fetches https://yourapp.com/api/account
  2. The victim visits evil.com while logged in to your app
  3. The browser sends the request with the victim's cookies
  4. Your server reflects Origin: https://evil.com and allows it
  5. The attacker's JavaScript reads the victim's account data

2. Misconfigured regex

A common attempt to whitelist multiple origins uses regex matching:

// ❌ Misconfigured regex
const allowedOrigin = /yourapp\.com/.test(origin);

This matches yourapp.com — but also evil-yourapp.com, yourapp.com.evil.com, and yourapp.company. The regex is too loose.

// ✅ Correct regex
const allowedOrigin = /^https:\/\/(www\.)?yourapp\.com$/.test(origin);

3. Trusting the null origin

Some servers whitelist the null origin for development convenience:

if (origin === 'null') {
  res.header('Access-Control-Allow-Origin', 'null');
}

The null origin is sent by sandboxed iframes, file:// URLs, and certain redirects. An attacker can send a request with null origin from a sandboxed iframe on their site and bypass your CORS policy.

Never trust null in a production CORS policy.

4. Broad subdomain wildcards

Access-Control-Allow-Origin: https://*.yourapp.com

If any subdomain is compromised (including a decommissioned one), or if you have a subdomain takeover vulnerability, the attacker gets CORS access to your main API.

Be explicit about which subdomains need access.

5. Overly permissive preflight

Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *

While less dangerous than origin issues, permitting all methods and headers unnecessarily expands your attack surface.

Correct CORS Configuration

Explicit allowlist

const ALLOWED_ORIGINS = new Set([
  'https://yourapp.com',
  'https://www.yourapp.com',
  'https://dashboard.yourapp.com',
]);

function corsMiddleware(req, res, next) {
  const origin = req.headers.origin;

  if (origin && ALLOWED_ORIGINS.has(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Vary', 'Origin'); // Critical: tell caches this varies by origin
  }

  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Access-Control-Max-Age', '86400');
    return res.sendStatus(204);
  }

  next();
}

The Vary: Origin header

When you dynamically set Access-Control-Allow-Origin based on the request origin, you must also set Vary: Origin. Without it, CDNs and proxies may cache a response with one origin's CORS headers and serve it to a different origin.

Express with cors package

import cors from 'cors';

const corsOptions = {
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.has(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsOptions));

Next.js API routes

// app/api/data/route.ts
const ALLOWED_ORIGINS = ['https://yourapp.com', 'https://www.yourapp.com'];

export async function GET(request: Request) {
  const origin = request.headers.get('origin') ?? '';
  const allowed = ALLOWED_ORIGINS.includes(origin);

  return Response.json(
    { data: '...' },
    {
      headers: {
        'Access-Control-Allow-Origin': allowed ? origin : '',
        'Access-Control-Allow-Credentials': 'true',
        Vary: 'Origin',
      },
    }
  );
}

export async function OPTIONS(request: Request) {
  const origin = request.headers.get('origin') ?? '';
  const allowed = ALLOWED_ORIGINS.includes(origin);

  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': allowed ? origin : '',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Max-Age': '86400',
      Vary: 'Origin',
    },
  });
}

Testing CORS Configuration

# Test a specific origin
curl -H "Origin: https://evil.com" \
     -H "Cookie: session=yourtoken" \
     -v https://yourapi.com/api/account 2>&1 | grep -i "access-control"

# Test preflight
curl -X OPTIONS \
     -H "Origin: https://yourapp.com" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type" \
     -v https://yourapi.com/api/data

Public APIs vs Authenticated APIs

If your API is public (no authentication, no sensitive data):

Access-Control-Allow-Origin: *

This is fine. Anyone can read the data anyway.

If your API is authenticated (cookies, session tokens, or any user-specific data):

  • Never use *
  • Always use an explicit allowlist
  • Always add Access-Control-Allow-Credentials: true only with specific origins
  • Always add Vary: Origin
cors
api-security
misconfiguration
web-security
javascript

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.