Skip to main content

Configure CORS Headers: Secure React API Setup

Misconfigured CORS is a security vulnerability. Setting Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true opens your API to cross-origin credential theft. Setting Access-Control-Allow-Methods: * and Access-Control-Allow-Headers: * exposes your API to unintended use. This article teaches you the safe patterns and anti-patterns for configuring CORS on your backend API so your React apps can call it securely.

In 2026, every production API must have a deliberate CORS policy. Permissive defaults are not acceptable. (OWASP, 2026)

Safe CORS Configuration Pattern

The safest pattern is:

  1. Specify exact origins (no wildcards). List the domains from which your React apps run.
  2. Specify exact methods (GET, POST, PUT, DELETE, PATCH).
  3. Specify exact headers (Content-Type, Authorization, X-Custom-Header).
  4. Set Credentials: true only if you use cookies or other credentials.
  5. Cache preflights with Max-Age.
// Node.js/Express: Safe CORS middleware
const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = [
'https://app.example.com',
'https://dashboard.example.com',
'http://localhost:3000', // Local development only
];

const corsOptions = {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Allow cookies if using session-based auth
maxAge: 3600, // Cache preflight for 1 hour
};

app.use(cors(corsOptions));

app.get('/api/users', (req, res) => {
res.json({ users: [] });
});

app.listen(3001);

This configuration:

  • Allows requests from exactly three origins (app.example.com, dashboard.example.com, localhost:3000).
  • Rejects requests from any other origin.
  • Allows GET, POST, PUT, DELETE, PATCH (not CONNECT, TRACE, HEAD unless explicitly needed).
  • Allows Content-Type and Authorization headers (the most common headers).
  • Permits credentials (cookies/authentication).
  • Caches preflight responses for 1 hour.

Anti-Patterns: What NOT to Do

Anti-pattern 1: Wildcard origin with credentials

// DANGEROUS - DO NOT USE
app.use(cors({
origin: '*',
credentials: true,
}));

This allows any origin to make requests with your user's credentials. An attacker at evil.com can fetch your API with your session cookies, bypassing the same-origin policy.

Anti-pattern 2: Reflecting the Origin header

Some developers copy the Origin header from the request to the response:

// DANGEROUS - DO NOT USE
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
next();
});

This opens your API to any origin, including evil.com. Always use an allow-list.

Anti-pattern 3: Wildcard methods and headers with specific origins

// RISKY - Use with caution
app.use(cors({
origin: 'https://app.example.com',
methods: '*', // Allows all methods
allowedHeaders: '*', // Allows all headers
credentials: true,
}));

This allows your app to send any HTTP method and any custom header, potentially exposing internal APIs or debugging endpoints you did not intend to make public.

Configuring CORS for Different Scenarios

Scenario 1: Public API (no authentication required)

const corsOptions = {
origin: '*', // Allow any origin
methods: ['GET'], // Read-only
maxAge: 86400, // Cache preflight for 1 day
};

app.use(cors(corsOptions));

This is safe because the API is public (no credentials) and only allows reads.

Scenario 2: Private API with session authentication (cookies)

const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type'],
credentials: true, // Include cookies
maxAge: 3600,
};

app.use(cors(corsOptions));

Credentials are enabled, so cookies are sent. Only explicitly listed origins can access the API.

Scenario 3: API with Bearer token authentication (no cookies)

const corsOptions = {
origin: ['https://app.example.com', 'https://mobile.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: false, // No cookies
maxAge: 3600,
};

app.use(cors(corsOptions));

No credentials are sent (tokens go in the Authorization header). The API is more flexible because the token is application-specific, not browser-managed.

Handling Credentials in React

When making cross-origin requests from React, remember to include credentials:

// React fetch without credentials
fetch('https://api.example.com/public-data')
.then(res => res.json());

// React fetch with credentials (cookies)
fetch('https://api.example.com/user-data', {
credentials: 'include', // Send cookies
})
.then(res => res.json());

// React fetch with Bearer token (no cookies)
fetch('https://api.example.com/user-data', {
headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
})
.then(res => res.json());

The backend CORS configuration must match your frontend's credential strategy.

Dynamic Origin Allow-List

For production, load the allow-list from an environment variable or database:

const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [
'https://app.example.com',
];

const corsOptions = {
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS not allowed'));
}
},
credentials: true,
maxAge: 3600,
};

app.use(cors(corsOptions));

Set ALLOWED_ORIGINS as an environment variable:

ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com node server.js

Key Takeaways

  • Always use an allow-list of origins; never use * with credentials: true.
  • Specify exact methods (GET, POST, PUT, DELETE); avoid wildcard methods.
  • Specify exact headers (Content-Type, Authorization); avoid wildcard headers.
  • Use environment variables to manage the allow-list; never hard-code production origins.
  • Set credentials: true only if using cookies; use Authorization headers for tokens.
  • Cache preflight responses with maxAge to reduce overhead.

Frequently Asked Questions

Can I use Access-Control-Allow-Origin: * if my API is public and requires no authentication?

Yes. This is safe if the API is read-only and returns only public data. The wildcard is acceptable because there is no credential risk. However, even public APIs benefit from restricting methods and headers to prevent abuse.

How do I allow multiple origins with Access-Control-Allow-Origin?

The header accepts a single origin, not a list. Use an allow-list in your middleware and set the header to the requesting origin (if allowed). See the safe pattern above.

If I set Access-Control-Allow-Credentials: true, do cookies get sent automatically?

Only if the React component uses credentials: 'include' in fetch(). Without this, the browser will not send cookies even if the server allows credentials.

What if I need to allow any subdomain of my site (e.g., *.example.com)?

The Access-Control-Allow-Origin header does not support wildcards with subdomains. You must list each subdomain explicitly or check the Origin header dynamically (as shown in the safe pattern) and allow origins matching a regex.

Further Reading