CORS Security

CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by modern browsers that controls how web applications can make HTTP requests to domains different from the one that served the original page, relaxing the Same-Origin Policy (SOP) in a controlled manner - a fundamental security policy that restricts scripts from one origin from accessing resources of another origin. Although CORS is essential for modern web application architectures, where front-ends frequently need to consume APIs hosted on different domains, its improper configuration represents one of the most common and dangerous vulnerabilities in contemporary web applications. CORS misconfigurations can expose sensitive data to unauthorized domains, enable Cross-Site Request Forgery (CSRF) attacks even in the presence of anti-CSRF tokens, facilitate credential theft, and in extreme cases, allow attackers to perform privileged actions on behalf of authenticated users. The problem is aggravated by the fact that many developers, upon encountering CORS errors during development, opt for overly permissive solutions (such as using the wildcard "*" or automatically reflecting the request origin) without fully understanding the security implications. This article explores in depth the fundamentals of CORS, common configuration vulnerabilities, and establishes robust practices for secure implementation across different platforms and frameworks, balancing functionality with an adequate defensive posture.

Same-Origin Policy (SOP)

Browsers implement SOP: scripts can only access resources from the same origin (protocol + domain + port). CORS relaxes SOP in a controlled manner.

      # Same origin
      https://example.com/api ← https://example.com/app [OK]
      # Different origins (blocked by SOP)
      https://example.com ← http://example.com (protocol)
      https://example.com ← https://api.example.com (subdomain)
      https://example.com ← https://example.com:8080 (port)
      

CORS Headers

Access-Control-Allow-Origin

      # Allow specific origin (recommended)
      Access-Control-Allow-Origin: https://trusted.com
      # Allow any origin (DANGEROUS!)
      Access-Control-Allow-Origin: *
      # Dynamic based on whitelist (correct)
      const allowedOrigins = ['https://app1.com', 'https://app2.com'];
      const origin = request.headers.origin;
      if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      

Other Important Headers

      # Allow credentials (cookies, auth headers)
      Access-Control-Allow-Credentials: true
      # Allowed HTTP methods
      Access-Control-Allow-Methods: GET, POST, PUT, DELETE
      # Allowed headers in requests
      Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
      # Headers exposed to client-side JavaScript
      Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
      # Preflight cache time (seconds)
      Access-Control-Max-Age: 86400
      

Preflight Requests

Browsers send an OPTIONS request before "non-simple" requests to verify permissions.

      # Client sends preflight
      OPTIONS /api/resource HTTP/1.1
      Origin: https://app.com
      Access-Control-Request-Method: DELETE
      Access-Control-Request-Headers: Authorization
      # Server responds with permissions
      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: https://app.com
      Access-Control-Allow-Methods: GET, POST, DELETE
      Access-Control-Allow-Headers: Authorization
      Access-Control-Max-Age: 86400
      

Common CORS Vulnerabilities

1. Wildcard with Credentials

      # [ERROR] VULNERABLE - does not work and is dangerous
      Access-Control-Allow-Origin: *
      Access-Control-Allow-Credentials: true
      # Browsers block this combination
      # [OK] CORRECT - specific origin with credentials
      Access-Control-Allow-Origin: https://trusted.com
      Access-Control-Allow-Credentials: true
      

2. Reflection Attack

      # [ERROR] VULNERABLE - reflects any origin
      const origin = request.headers.origin;
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      # [OK] CORRECT - whitelist validation
      const allowedOrigins = ['https://app.com', 'https://admin.com'];
      const origin = request.headers.origin;
      if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      }
      

3. Subdomain Wildcard

      # [ERROR] VULNERABLE - poorly implemented regex
      const origin = request.headers.origin;
      if (/https:\/\/.*\.example\.com/.test(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      // Accepts https://evil.example.com.attacker.com
      # [OK] CORRECT - strict validation
      const origin = request.headers.origin;
      if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      

Secure Configuration by Technology

Node.js/Express (CORS middleware)

      const cors = require('cors');
      // Secure configuration
      const corsOptions = {
      origin: function (origin, callback) {
      const allowedOrigins = [
      'https://app.example.com',
      'https://admin.example.com'
      ];
      if (!origin || allowedOrigins.includes(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
      };
      app.use(cors(corsOptions));
      

Nginx

      # Conditional configuration
      map $http_origin $cors_origin {
      default "";
      "~^https://app\\.example\\.com$" $http_origin;
      "~^https://admin\\.example\\.com$" $http_origin;
      }
      server {
      location /api {
      if ($cors_origin != "") {
      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" always;
      add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
      }
      if ($request_method = OPTIONS) {
      return 204;
      }
      }
      }
      

Apache

      # .htaccess
      SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
      Header always set Access-Control-Allow-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" env=CORS_ORIGIN
      Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
      # Respond to OPTIONS preflight
      RewriteEngine On
      RewriteCond % OPTIONS
      RewriteRule ^(.*)$ $1 [R=204,L]
      

Testing CORS

      # Test with curl
      curl -H "Origin: https://evil.com" \\
      -H "Access-Control-Request-Method: DELETE" \\
      -H "Access-Control-Request-Headers: Authorization" \\
      -X OPTIONS \\
      https://api.example.com/resource
      # JavaScript test
      fetch('https://api.example.com/data', {
      method: 'GET',
      credentials: 'include',
      headers: {
      'Content-Type': 'application/json'
      }
      }).then(response => console.log(response));
      

Best Practices

  • Never use the wildcard (*) in APIs with sensitive data
  • Explicit whitelist of allowed origins
  • Strict validation of origin with secure regex
  • Minimize credentials: Only enable if truly necessary
  • Least privilege: Allow only the necessary methods and headers
  • Cache preflight: Use Max-Age to reduce overhead
  • Monitoring: Log suspicious CORS rejections

Secure CORS Checklist

  • [OK] Origin validated against an explicit whitelist
  • [OK] Validation regex does not allow bypasses
  • [OK] Credentials only enabled when necessary
  • [OK] Methods and headers restricted to the minimum
  • [OK] Preflight configured correctly
  • [OK] Tested against malicious origins
  • [OK] Logs of rejected requests monitored