Home/Blog/CORS Security Guide: Preventing Cross-Origin Attacks and

CORS Security Guide: Preventing Cross-Origin Attacks and

Learn how to implement secure CORS policies, avoid common misconfigurations like wildcard origins and origin reflection, and protect your APIs from cross-origin attacks.

By Inventive HQ Team

Cross-Origin Resource Sharing (CORS) is one of the most misunderstood and misconfigured security mechanisms in web applications. According to recent security research, nearly 90% of API breaches involve misconfigured CORS policies. This guide explains how CORS works, common vulnerabilities, and how to implement secure configurations.

What is CORS and Why Does It Matter?

CORS is a browser security mechanism that controls how web pages from one origin (domain) can request resources from another origin. Without CORS, the browser's same-origin policy would block all cross-origin requests, making modern web applications impossible.

Here's what happens during a cross-origin request:

  1. Browser sends request with Origin header
  2. Server responds with Access-Control-Allow-Origin header
  3. Browser compares origins and allows or blocks the response
Request:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Key CORS Headers Explained

HeaderPurposeExample
Access-Control-Allow-OriginSpecifies allowed originshttps://example.com or *
Access-Control-Allow-CredentialsAllows cookies/auth headerstrue
Access-Control-Allow-MethodsAllowed HTTP methodsGET, POST, PUT
Access-Control-Allow-HeadersAllowed request headersContent-Type, Authorization
Access-Control-Max-AgePreflight cache duration (seconds)86400
VaryCache key for responsesOrigin

Understanding Preflight Requests

Browsers send preflight OPTIONS requests before "complex" cross-origin requests. A request is considered complex if it:

  • Uses methods other than GET, HEAD, or POST
  • Includes custom headers
  • Uses Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain
Preflight Request:
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

Preflight Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Common CORS Misconfigurations

1. Wildcard Origin with Credentials (Critical)

The vulnerability:

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

This configuration is impossible according to the CORS specification - browsers reject it. However, many developers work around this by reflecting any origin, which is even worse.

Why it's dangerous: Any website can make authenticated requests to your API using the victim's cookies.

Attack scenario:

  1. User is logged into your application
  2. User visits attacker's website
  3. Attacker's JavaScript makes requests to your API
  4. Browser includes user's cookies automatically
  5. Attacker receives sensitive data from your API

2. Origin Reflection (Critical)

The vulnerability:

# Dangerous: Reflects any origin
allowed_origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = allowed_origin
response.headers['Access-Control-Allow-Credentials'] = 'true'

Why it's dangerous: The server accepts ANY origin, completely bypassing CORS protection.

Secure alternative:

ALLOWED_ORIGINS = {'https://app.example.com', 'https://admin.example.com'}

origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
    response.headers['Access-Control-Allow-Origin'] = origin
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    response.headers['Vary'] = 'Origin'

3. Null Origin Acceptance (High)

The vulnerability:

Access-Control-Allow-Origin: null

Why it's dangerous: The null origin can be triggered from:

  • Sandboxed iframes (<iframe sandbox="allow-scripts">)
  • Local file:// URLs
  • Data URLs
  • Redirects from HTTP to HTTPS

Attackers can craft pages that send requests with a null origin.

4. Trusting All Subdomains (Medium)

The vulnerability:

origin = request.headers.get('Origin')
if origin and origin.endswith('.example.com'):
    # Accepts evil.example.com, anything.example.com, etc.
    response.headers['Access-Control-Allow-Origin'] = origin

Why it's dangerous: If any subdomain is compromised (XSS, subdomain takeover), attackers can access your API.

Secure alternative:

ALLOWED_SUBDOMAINS = {'app.example.com', 'api.example.com'}

parsed = urlparse(origin)
if parsed.netloc in ALLOWED_SUBDOMAINS:
    response.headers['Access-Control-Allow-Origin'] = origin

5. Missing Vary Header (Medium)

The vulnerability: Not including Vary: Origin when the CORS response varies by origin.

Why it's dangerous: CDNs and browsers may cache responses with the wrong CORS headers, causing legitimate requests to fail or allowing unauthorized access.

Fix: Always include Vary: Origin when your CORS response depends on the request origin:

Vary: Origin

Implementing Secure CORS

Step 1: Define Your Origin Whitelist

// Express.js example
const allowedOrigins = new Set([
  'https://app.example.com',
  'https://admin.example.com',
  'https://mobile.example.com'
]);

Step 2: Validate Origins Strictly

const cors = require('cors');

const corsOptions = {
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl)
    if (!origin) return callback(null, true);

    if (allowedOrigins.has(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400 // 24 hours
};

app.use(cors(corsOptions));

Step 3: Restrict Methods and Headers

Only allow methods and headers your API actually needs:

methods: ['GET', 'POST'],  // Not PUT, DELETE unless needed
allowedHeaders: ['Content-Type', 'Authorization']

Step 4: Add Vary Header

Ensure proper caching behavior:

app.use((req, res, next) => {
  res.header('Vary', 'Origin');
  next();
});

Step 5: Set Reasonable Max-Age

Balance security with performance:

maxAge: 86400  // 24 hours - re-validate daily

Testing Your CORS Configuration

Using curl

# Test basic CORS
curl -I -X OPTIONS https://api.example.com/endpoint \
  -H "Origin: https://evil.com" \
  -H "Access-Control-Request-Method: GET"

# Test with credentials
curl -I https://api.example.com/endpoint \
  -H "Origin: https://evil.com" \
  -H "Cookie: session=abc123"

What to Check

  1. Wildcard with credentials: Does the API return * with credentials: true?
  2. Origin reflection: Does the API echo back any origin you send?
  3. Null origin: Does the API accept Origin: null?
  4. Subdomain bypass: Does evil.example.com get accepted?
  5. Method exposure: Are dangerous methods (DELETE, PATCH) allowed?
  6. Vary header: Is Vary: Origin present?

Automated Testing

Include CORS tests in your CI/CD pipeline:

describe('CORS Security', () => {
  it('should reject unauthorized origins', async () => {
    const response = await fetch('https://api.example.com/data', {
      headers: { 'Origin': 'https://evil.com' }
    });
    expect(response.headers.get('Access-Control-Allow-Origin'))
      .not.toBe('https://evil.com');
  });

  it('should not reflect arbitrary origins', async () => {
    const response = await fetch('https://api.example.com/data', {
      headers: { 'Origin': 'https://random-attacker.com' }
    });
    expect(response.headers.get('Access-Control-Allow-Origin'))
      .toBeNull();
  });
});

Framework-Specific Configurations

Express.js

const cors = require('cors');

app.use(cors({
  origin: ['https://app.example.com'],
  credentials: true,
  methods: ['GET', 'POST'],
  maxAge: 86400
}));

Django

# settings.py
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_PREFLIGHT_MAX_AGE = 86400

Flask

from flask_cors import CORS

CORS(app,
     origins=['https://app.example.com'],
     supports_credentials=True,
     max_age=86400)

Spring Boot

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowCredentials(true)
            .allowedMethods("GET", "POST")
            .maxAge(86400);
    }
}

CORS Security Checklist

  • Origin whitelist defined (no wildcards in production)
  • No origin reflection (server-side validation, not echo)
  • Null origin rejected
  • Subdomain validation uses exact matching
  • Credentials only for specific trusted origins
  • HTTP methods restricted to minimum needed
  • Custom headers restricted to minimum needed
  • Vary: Origin header present
  • Max-Age set to reasonable value (≤24 hours)
  • CORS configuration tested in CI/CD
  • Regular security audits include CORS review

Conclusion

CORS misconfigurations are among the most common and dangerous web security vulnerabilities. By implementing strict origin whitelists, avoiding origin reflection, and testing your configuration regularly, you can protect your APIs from cross-origin attacks.

Use our CORS Policy Analyzer to test your current configuration and identify vulnerabilities before attackers do.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.