04 - Secure CORS Implementation & Mitigations
To secure applications against CORS vulnerabilities, developers must move away from dynamic origin reflection and permissive wildcards. Production environments require strict, server-side allowlists, accurate regex boundaries, mandatory Vary: Origin headers, and secure gateway configurations.
1. Defensive Architectural Rules
┌──────────────────────────────────────────────────────────────────────────┐
│ PRODUCTION CORS SECURITY GOLD RULES │
├──────────────────────────────────────────────────────────────────────────┤
│ 1. Enforce Strict Allowlists: Maintain an explicit, hardcoded list of │
│ authorized domains. Validate exact origin strings via O(1) set/map. │
├──────────────────────────────────────────────────────────────────────────┤
│ 2. Never Trust 'null': Reject the `null` origin string unconditionally. │
├──────────────────────────────────────────────────────────────────────────┤
│ 3. Never Combine Wildcards with Credentials: If ACAC is true, ACAO MUST │
│ be an exact explicit origin string, never `*`. │
├──────────────────────────────────────────────────────────────────────────┤
│ 4. Anchor Regex Patterns: If domain matching requires regex, explicitly │
│ anchor with `^` and `$`, and escape all domain literal dots (`\.`). │
├──────────────────────────────────────────────────────────────────────────┤
│ 5. Always Append `Vary: Origin`: Ensure CDNs and intermediate caches do │
│ not serve cached CORS headers to unauthorized origins. │
├──────────────────────────────────────────────────────────────────────────┤
│ 6. Minimize Preflight Max-Age: Restrict `Access-Control-Max-Age` to │
│ reasonable limits (e.g., 600 - 3600 seconds) to enable fast revoking. │
└──────────────────────────────────────────────────────────────────────────┘
2. Multi-Language Implementation Snippets
A. Node.js (Express.js)
❌ Insecure Pattern (Dynamic Reflection)
// INSECURE: Echoing Origin header blindly
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
✅ Secure Production Pattern (cors middleware)
const express = require('express');
const cors = require('cors');
const app = express();
// Strict explicit allowlist
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com'
]);
const corsOptions = {
origin: (origin, callback) => {
// Allow requests with no origin (e.g., mobile native apps, cURL, server-to-server)
if (!origin) {
return callback(null, true);
}
if (ALLOWED_ORIGINS.has(origin)) {
callback(null, true); // Origin authorized
} else {
callback(new Error('CORS Policy Violation: Origin not allowed'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count'],
maxAge: 600, // 10 minutes
optionsSuccessStatus: 204
};
// Apply CORS middleware globally
app.use(cors(corsOptions));
// Ensure Vary: Origin header is set on all responses
app.use((req, res, next) => {
res.header('Vary', 'Origin');
next();
});
app.get('/api/v1/data', (req, res) => {
res.json({ status: "success", data: "Protected Content" });
});
B. Python (FastAPI)
❌ Insecure Pattern (allow_origins=["*"] + Credentials)
# INSECURE: Wildcard origins with credentials set to True
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True, # Triggers security issues or browser errors
)
✅ Secure Production Pattern
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI(title="Secure API")
# Explicit production allowlist
ALLOWED_ORIGINS = [
"https://app.example.com",
"https://dashboard.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Total-Count"],
max_age=600,
)
# Custom Middleware to enforce Vary: Origin
class VaryOriginMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["Vary"] = "Origin"
return response
app.add_middleware(VaryOriginMiddleware)
@app.get("/api/v1/user")
async def get_user_profile():
return {"user_id": 42, "role": "administrator"}
C. Python (Flask)
✅ Secure Production Pattern (Flask-CORS)
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# Strict dictionary-based CORS configuration
CORS_CONFIG = {
"origins": [
"https://app.example.com",
"https://portal.example.com"
],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"expose_headers": ["X-Total-Count"],
"supports_credentials": True,
"max_age": 600
}
# Apply strictly to /api/* routes
CORS(app, resources={r"/api/*": CORS_CONFIG})
@app.after_request
def add_vary_header(response):
response.headers['Vary'] = 'Origin'
return response
@app.route('/api/profile', methods=['GET'])
def get_profile():
return jsonify({"username": "alice", "email": "alice@example.com"})
D. Go (Gin Framework & Standard net/http)
✅ Secure Gin Middleware (github.com/gin-contrib/cors)
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Configure strict CORS rules
config := cors.Config{
AllowOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"X-Total-Count"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
router.Use(cors.New(config))
// Custom Vary Header Middleware
router.Use(func(c *gin.Context) {
c.Header("Vary", "Origin")
c.Next()
})
router.GET("/api/v1/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "healthy"})
})
router.Run(":8080")
}
✅ Secure Standard net/http Middleware
package main
import (
"net/http"
)
var allowedOrigins = map[string]bool{
"https://app.example.com": true,
"https://portal.example.com": true,
}
func SecureCORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Always set Vary: Origin for caching proxies
w.Header().Set("Vary", "Origin")
if origin != "" && allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
}
// Handle Preflight OPTIONS requests directly
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
E. Java (Spring Boot)
✅ Secure WebMvcConfigurer Configuration
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SecurityCorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
3. Reverse Proxy & API Gateway Configurations
A. Nginx Reverse Proxy
To manage CORS securely at the API gateway level without relying on backend code, use an Nginx map block for O(1) origin matching.
[!IMPORTANT] Avoid Nginx
ifStatements in Location Blocks: Usingifdirectives inside Nginx location blocks can cause unexpected side effects. Using amapdirective in thehttpcontext is clean, secure, and performant.
http {
# Define an explicit origin allowlist map
map `$http_origin $`cors_allowed_origin {
default "";
"https://app.example.com" $http_origin;
"https://dashboard.example.com" $http_origin;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL Configuration omitted for brevity
location /api/ {
# Set Vary header to prevent CDN cache poisoning
add_header 'Vary' 'Origin' always;
# Check if request is Preflight OPTIONS
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_allowed_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' 'Authorization, Content-Type, X-Request-ID' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# Actual Request Headers
add_header 'Access-Control-Allow-Origin' $cors_allowed_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend_upstream;
}
}
}
B. Apache HTTP Server
# In Apache Configuration or .htaccess
<IfModule mod_headers.c>
# Set Vary: Origin
Header always set Vary "Origin"
# Match allowed origin explicitly
SetEnvIf Origin "^https://(app|portal)\.example\.com`$" CORS_ALLOWED_ORIGIN=$`0
Header always set Access-Control-Allow-Origin "%{CORS_ALLOWED_ORIGIN}e" env=CORS_ALLOWED_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ALLOWED_ORIGIN
</IfModule>