Understanding CORS and Cross-Origin Requests

Meta Description: Understand CORS (Cross-Origin Resource Sharing) for REST APIs. Learn preflight requests, credentials, and how to configure CORS securely. Keywords: cors, cross-origin requests, cors headers, preflight requests, api cors, cors configuration Word Count: ~2,200 words Your API works perfectly in Postman. But when you call it from a

TRY NANO BANANA FOR FREE

Understanding CORS and Cross-Origin Requests

TRY NANO BANANA FOR FREE
Contents

Meta Description: Understand CORS (Cross-Origin Resource Sharing) for REST APIs. Learn preflight requests, credentials, and how to configure CORS securely.

Keywords: cors, cross-origin requests, cors headers, preflight requests, api cors, cors configuration

Word Count: ~2,200 words


Your API works perfectly in Postman. But when you call it from a browser, you get:

Access to fetch at 'https://api.petstoreapi.com/v1/pets' from origin
'https://myapp.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

This is CORS (Cross-Origin Resource Sharing). It's a browser security feature that blocks requests to different domains.

Here's how to fix it correctly.

What Is CORS?

CORS is a browser security mechanism. It prevents malicious websites from making unauthorized requests to your API.

Same-Origin Policy

Browsers enforce the same-origin policy. JavaScript can only make requests to the same origin (protocol + domain + port).

Same origin (allowed):

Page: https://myapp.com/dashboard
API:  https://myapp.com/api/pets  ✓

Different origin (blocked):

Page: https://myapp.com/dashboard
API:  https://api.petstoreapi.com/v1/pets  ✗

Without CORS, the second request fails.

Why Same-Origin Policy Exists

Imagine you're logged into your bank at bank.com. You visit a malicious site evil.com. Without same-origin policy:

// On evil.com
fetch('https://bank.com/api/transfer', {
  method: 'POST',
  credentials: 'include', // Sends your bank.com cookies
  body: JSON.stringify({
    to: 'attacker-account',
    amount: 10000
  })
});

The browser would send your authentication cookies to bank.com, and the attacker could steal your money.

Same-origin policy prevents this. Requests from evil.com to bank.com are blocked.

How CORS Works

CORS lets servers explicitly allow cross-origin requests.

Simple Requests

For simple requests (GET, POST with simple headers), the browser:

  1. Sends the request with an Origin header
  2. Server responds with Access-Control-Allow-Origin
  3. Browser allows or blocks based on the header

Request:

GET /v1/pets HTTP/1.1
Host: api.petstoreapi.com
Origin: https://myapp.com

Response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json

[{"id":"123","name":"Max"}]

The browser sees Access-Control-Allow-Origin: https://myapp.com and allows the response.

Preflight Requests

For complex requests (PUT, DELETE, custom headers), the browser sends a preflight request first.

Preflight (OPTIONS request):

OPTIONS /v1/pets/123 HTTP/1.1
Host: api.petstoreapi.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

Preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400

If the preflight succeeds, the browser sends the actual request.

Configuring CORS

Allow All Origins (Development Only)

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

Warning: * allows any website to call your API. Only use this for public APIs without authentication.

Allow Specific Origins (Production)

const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com',
  'http://localhost:3000' // Development
];

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

Handle Preflight Requests

app.options('*', (req, res) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight for 24 hours
  res.status(204).send();
});

Using CORS Middleware

const cors = require('cors');

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // Allow cookies
  maxAge: 86400 // Cache preflight for 24 hours
}));

CORS with Credentials

To send cookies or authentication headers, you need special configuration.

Client Side

fetch('https://api.petstoreapi.com/v1/pets', {
  credentials: 'include' // Send cookies
});

Server Side

app.use(cors({
  origin: 'https://myapp.com', // Must be specific, not '*'
  credentials: true
}));

Important: When credentials: true, you cannot use Access-Control-Allow-Origin: *. You must specify exact origins.

Common CORS Issues

Issue 1: Wildcard with Credentials

Error:

The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.

Fix: Use specific origins, not *:

res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');

Issue 2: Missing Preflight Headers

Error:

Request header field authorization is not allowed by
Access-Control-Allow-Headers in preflight response.

Fix: Add the header to allowed headers:

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

Issue 3: Preflight Cache Not Working

Browsers cache preflight responses. If you change CORS config, clients might use stale cache.

Fix: Set Access-Control-Max-Age appropriately:

res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours

For development, use 0 to disable caching:

res.setHeader('Access-Control-Max-Age', '0');

Issue 4: Custom Headers Not Exposed

By default, browsers only expose safe response headers. Custom headers are hidden.

Fix: Use Access-Control-Expose-Headers:

res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Number');

Now JavaScript can read these headers:

const response = await fetch('/v1/pets');
const totalCount = response.headers.get('X-Total-Count');

CORS Security Best Practices

1. Validate Origins Strictly

Don't use regex or substring matching:

Bad:

// Allows evil-myapp.com!
if (origin.includes('myapp.com')) {
  res.setHeader('Access-Control-Allow-Origin', origin);
}

Good:

const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
}

2. Don't Reflect Origin Blindly

Bad:

// Allows any origin!
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);

Good:

if (allowedOrigins.includes(req.headers.origin)) {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}

3. Use HTTPS

CORS doesn't protect against man-in-the-middle attacks. Always use HTTPS for both your app and API.

4. Limit Allowed Methods

Only allow methods you actually use:

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// Don't add TRACE, CONNECT, or other methods you don't use

5. Limit Allowed Headers

Only allow headers you need:

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Don't allow all headers

CORS Alternatives

1. Proxy Server

Run a proxy on your domain that forwards requests to the API:

Browser → https://myapp.com/api/pets → Proxy → https://api.petstoreapi.com/v1/pets

No CORS issues because requests stay on the same origin.

2. JSONP (Deprecated)

JSONP bypasses CORS by using <script> tags. Don't use it—it's insecure and deprecated.

3. Server-Side Requests

Make API calls from your backend, not the browser:

Browser → Your Backend → API

No CORS because browsers aren't involved.

Testing CORS

Using cURL

# Simple request
curl -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: authorization" \
  -X OPTIONS \
  https://api.petstoreapi.com/v1/pets

Check for Access-Control-Allow-Origin in the response.

Using Browser DevTools

  1. Open DevTools → Network tab
  2. Make a cross-origin request
  3. Check the request headers (should include Origin)
  4. Check the response headers (should include Access-Control-Allow-Origin)

Using Online Tools

Summary

CORS is a browser security feature that blocks cross-origin requests by default.

To allow cross-origin requests: 1. Set Access-Control-Allow-Origin header 2. Handle OPTIONS preflight requests 3. Set Access-Control-Allow-Methods and Access-Control-Allow-Headers

For credentials (cookies, auth headers): 1. Set Access-Control-Allow-Credentials: true2. Use specific origins, not *3. Set credentials: 'include' on the client

Security: - Validate origins strictly - Use HTTPS - Limit allowed methods and headers - Don't reflect origins blindly

CORS protects users from malicious websites. Configure it correctly to balance security and functionality.