AI QA Monkey
AI Security Intelligence
Fix Guide

How to Fix CORS Misconfiguration on Apache, Nginx & Node.js

Cross-Origin Resource Sharing (CORS) is one of the most misunderstood — and most exploited — browser security mechanisms. A single misconfigured header can allow an attacker's website to make authenticated API requests on behalf of your users, stealing sensitive data without any user interaction.

According to OWASP, Security Misconfiguration is the #5 risk in the 2021 Top 10, and CORS misconfigurations are among the most common findings in penetration tests. In our analysis of over 50,000 scans on AI QA Monkey, 23% of websites had at least one CORS misconfiguration.

This guide covers every common CORS misconfiguration, explains why it's dangerous, and provides copy-paste fix commands for Apache, Nginx, Node.js, Django, and Laravel.

What Is CORS and Why Does It Matter?

CORS is a browser security feature that controls which external domains can access resources on your server. Without CORS, the Same-Origin Policy blocks all cross-origin requests — meaning JavaScript on evil.com cannot read responses from your-api.com.

When you add CORS headers, you're explicitly telling browsers: "These specific origins are allowed to access my resources." The problem arises when developers set these headers too permissively to "make things work" during development — and then forget to tighten them for production.

How CORS Works (Simplified)

  1. Browser sends a request with an Origin header
  2. Server checks the origin and responds with Access-Control-Allow-Origin
  3. If the origin matches, the browser allows the response to be read by JavaScript
  4. For "complex" requests (PUT, DELETE, custom headers), the browser first sends a preflight OPTIONS request

The 5 Most Dangerous CORS Misconfigurations

1. Wildcard Origin with Credentials CRITICAL

# DANGEROUS — Never do this
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Why it's dangerous: This combination tells browsers "any website can make authenticated requests to my API." An attacker can create a page that silently reads your users' data, session tokens, or personal information. Modern browsers actually block this combination, but older browsers and some HTTP clients don't.

2. Reflecting the Origin Header Without Validation CRITICAL

# DANGEROUS — Reflects any origin
Access-Control-Allow-Origin: [whatever the request sends]
Access-Control-Allow-Credentials: true

Why it's dangerous: Some developers dynamically reflect the Origin header value back in the response without checking if it's a trusted domain. This effectively allows any website to make credentialed requests — it's functionally identical to the wildcard + credentials combination.

3. Null Origin Allowed HIGH

# DANGEROUS — null origin is exploitable
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

Why it's dangerous: The null origin can be triggered by sandboxed iframes, local file:// pages, and certain redirect chains. Attackers can craft pages that send requests with Origin: null to bypass your CORS policy.

4. Overly Broad Subdomain Wildcards HIGH

# DANGEROUS — regex matching *.example.com
if origin ends with ".example.com" → allow

Why it's dangerous: If an attacker finds an XSS vulnerability on any subdomain (e.g., blog.example.com), they can use it to make cross-origin requests to your API. Also, regex mistakes like matching notexample.com are common.

5. Missing Preflight Handling MEDIUM

# Server doesn't respond to OPTIONS requests
# Result: All non-simple cross-origin requests fail

Why it's a problem: While not a security vulnerability per se, failing to handle preflight requests breaks legitimate cross-origin functionality and often leads developers to "fix" it by adding wildcard CORS headers — creating a real vulnerability.

Fix CORS on Apache (.htaccess)

Secure Configuration

This configuration allows only specific trusted origins and properly handles preflight requests.

# .htaccess — Secure CORS Configuration
# Replace the allowed origins with your actual domains

# Define allowed origins
SetEnvIf Origin "^https://(www\.example\.com|app\.example\.com|staging\.example\.com)$" CORS_ORIGIN=$0

# Set CORS headers only for allowed origins
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" env=CORS_ORIGIN
Header always set Access-Control-Max-Age "86400" env=CORS_ORIGIN

# Handle preflight OPTIONS requests
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

# Prevent caching of CORS headers (important for CDNs)
Header always set Vary "Origin"

Fix CORS on Nginx

# nginx.conf — Secure CORS Configuration

# Map allowed origins
map $http_origin $cors_origin {
    default "";
    "https://www.example.com"     $http_origin;
    "https://app.example.com"     $http_origin;
    "https://staging.example.com" $http_origin;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    # Add CORS headers only for allowed origins
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Access-Control-Allow-Credentials "true" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
    add_header Access-Control-Max-Age 86400 always;
    add_header Vary "Origin" always;

    # Handle preflight requests
    if ($request_method = 'OPTIONS') {
        return 204;
    }

    location / {
        proxy_pass http://backend;
    }
}

Fix CORS on Node.js / Express

// Express.js — Secure CORS Configuration
const cors = require('cors');

const allowedOrigins = [
  'https://www.example.com',
  'https://app.example.com',
  'https://staging.example.com'
];

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

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

app.use(cors(corsOptions));

// Explicitly handle preflight for all routes
app.options('*', cors(corsOptions));

Fix CORS on Django

# settings.py — Django CORS Configuration
# Install: pip install django-cors-headers

INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Must be before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]

# Specific allowed origins (never use CORS_ALLOW_ALL_ORIGINS = True in production)
CORS_ALLOWED_ORIGINS = [
    'https://www.example.com',
    'https://app.example.com',
]

CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = [
    'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS',
]

CORS_ALLOW_HEADERS = [
    'content-type', 'authorization', 'x-requested-with',
]

# Cache preflight responses for 24 hours
CORS_PREFLIGHT_MAX_AGE = 86400

Fix CORS on Laravel

// config/cors.php — Laravel CORS Configuration
return [
    'paths' => ['api/*'],

    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],

    // Specific origins only — never use ['*'] with credentials
    'allowed_origins' => [
        'https://www.example.com',
        'https://app.example.com',
    ],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],

    'exposed_headers' => [],

    'max_age' => 86400,

    'supports_credentials' => true,
];

How to Test Your CORS Configuration

Manual Testing with curl

# Test if your API reflects arbitrary origins
curl -s -D - -o /dev/null \
  -H "Origin: https://evil-attacker.com" \
  https://your-api.com/endpoint

# Check for wildcard + credentials
curl -s -D - -o /dev/null \
  -H "Origin: https://evil-attacker.com" \
  https://your-api.com/endpoint | grep -i "access-control"

# Test preflight handling
curl -s -D - -o /dev/null \
  -X OPTIONS \
  -H "Origin: https://evil-attacker.com" \
  -H "Access-Control-Request-Method: PUT" \
  https://your-api.com/endpoint

Automated Testing with AI QA Monkey

The fastest way to test your CORS configuration is with AI QA Monkey's free API & CORS Security Scanner. It automatically tests for:

  • Wildcard origin with credentials
  • Origin reflection without validation
  • Null origin bypass
  • Subdomain wildcard patterns
  • Missing preflight handling
  • Exposed API endpoints and Swagger docs

Test Your CORS Configuration Now

Free scan — no signup required. Results in 60 seconds.

Scan Your API Now

Frequently Asked Questions

What is a CORS misconfiguration?

A CORS misconfiguration occurs when a web server's Cross-Origin Resource Sharing headers are set too permissively, allowing unauthorized websites to make requests to your API. The most critical misconfiguration is setting Access-Control-Allow-Origin to * (wildcard) while also enabling Access-Control-Allow-Credentials: true.

How do I fix CORS wildcard with credentials?

Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Instead, validate the Origin header against a whitelist of trusted domains and reflect only approved origins. See the Apache, Nginx, and Node.js configurations above for copy-paste solutions.

What is a CORS preflight request?

A preflight request is an HTTP OPTIONS request that browsers automatically send before certain cross-origin requests (those using methods like PUT/DELETE or custom headers). Your server must respond to OPTIONS requests with the correct Access-Control-Allow-Methods and Access-Control-Allow-Headers.

How do I test if my CORS configuration is secure?

Use AI QA Monkey's free API & CORS Security Scanner. It tests your CORS headers against known attack patterns. You can also test manually with curl -H "Origin: https://evil.com" -I https://your-api.com.

Check Your Website Right Now

Run a free automated security scan — 75 checks in 60 seconds. No signup required.

Run Free Security Scan →