CORS Configuration: Cross-Origin Resource Sharing Done Safely | SoniNow Blog

Limited TimeLearn More

corscross-originsecurityapibrowser

CORS Configuration: Cross-Origin Resource Sharing Done Safely

Published

2026-06-23

Read Time

6 mins

CORS Configuration: Cross-Origin Resource Sharing Done Safely

Cross-Origin Resource Sharing is a browser security mechanism that controls which web applications can access resources from a different origin. Misconfigured CORS — either too permissive or too restrictive — is one of the most common security findings in API security assessments.

How CORS Works

When a web application at https://app.example.com makes a cross-origin request to https://api.soninow.com, the browser sends an Origin header and checks the response for Access-Control-Allow-Origin. If the origin matches, the browser exposes the response to the calling JavaScript. If not, the browser blocks the response.

For requests that trigger side effects (non-simple methods like PUT, DELETE, or requests with custom headers), the browser first sends a preflight OPTIONS request to check permissions before sending the actual request.

# Preflight request (OPTIONS)
OPTIONS /api/v1/orders HTTP/1.1
Origin: https://app.soninow.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

# Preflight response (200)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.soninow.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

# Actual request
POST /api/v1/orders HTTP/1.1
Origin: https://app.soninow.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...

Understanding this flow is essential — the preflight is a browser-enforced gate, not a server security control. The server must still authenticate and authorize every request.

Origin Validation: Whitelist, Not Reflection

The most dangerous CORS misconfiguration is reflecting the Origin header back as Access-Control-Allow-Origin. This allows any website to read the response.

// DANGEROUS — never do this
// Reflecting origin allows any site to access your API
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});

Instead, implement an origin whitelist:

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

function corsMiddleware(req: Request, res: Response, next: NextFunction) {
  const origin = req.headers.origin;

  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin'); // Important for caching
  }

  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
    res.setHeader('Access-Control-Max-Age', '86400');
    return res.status(204).end();
  }

  next();
}

The Vary: Origin header ensures that caches differentiate responses by origin. Without it, a cached response for one allowed origin could be served to a different origin.

Credentialed Requests

When credentials (cookies, authorization headers, TLS client certificates) are included in cross-origin requests, CORS rules become stricter:

  • Access-Control-Allow-Origin must be an explicit origin (not *)
  • Access-Control-Allow-Credentials: true must be set
  • Access-Control-Allow-Origin in the preflight response must also be explicit
const corsOptions = {
  origin: ['https://app.soninow.com', 'https://admin.soninow.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
  exposedHeaders: ['X-RateLimit-Remaining', 'X-Request-Id'],
  maxAge: 86400,
};

When credentials are involved, wildcard origin is forbidden by the specification. The browser will reject any CORS response that uses * for Access-Control-Allow-Origin when credentials mode is include.

CORS for Public APIs

Public APIs intended for browser-based clients often need permissive CORS but should still be intentional:

const PUBLIC_CORS = {
  origin: '*',
  methods: ['GET'],
  allowedHeaders: ['Content-Type'],
  maxAge: 86400,
};

// Different CORS policy for authenticated endpoints
const AUTH_CORS = {
  origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
    if (!origin || ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
};

Even for public APIs, consider whether * is truly necessary. If you know the consuming applications, list them explicitly. A dynamic origin callback that validates against a database or configuration provides flexibility without sacrificing security.

Subdomain Handling

Subdomain access requires careful CORS configuration. A cookie set on app.soninow.com is not accessible from admin.soninow.com unless both domains are explicitly allowed.

// Allowlist with subdomain pattern matching
function isValidOrigin(origin: string): boolean {
  try {
    const url = new URL(origin);

    // Allow specific subdomains
    const allowedHosts = [
      'app.soninow.com',
      'admin.soninow.com',
      'www.soninow.com',
    ];

    if (allowedHosts.includes(url.hostname)) return true;

    // Allow development/staging subdomains with pattern
    if (/^[a-z0-9-]+\.soninow\.pages\.dev$/.test(url.hostname)) return true;

    return false;
  } catch {
    return false;
  }
}

For cookie-based authentication across subdomains, set the cookie domain to .soninow.com but keep individual CORS entries for each subdomain. This combination allows shared sessions while maintaining CORS specificity.

CORS and API Gateways

When using an API gateway (NGINX, Kong, AWS API Gateway, Cloudflare), CORS configuration may be handled at the gateway level. This can simplify application code but requires careful coordination.

# NGINX CORS configuration at reverse proxy level
server {
    listen 443 ssl;
    server_name api.soninow.com;

    location /api/ {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.soninow.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Request-Id' always;
            add_header 'Access-Control-Max-Age' 86400;
            return 204;
        }

        if ($request_method ~* '(GET|POST|PUT|DELETE)') {
            add_header 'Access-Control-Allow-Origin' 'https://app.soninow.com' always;
            add_header 'Access-Control-Allow-Credentials' 'true' always;
        }

        proxy_pass http://backend;
    }
}

If CORS is handled by the gateway, ensure the application does not add conflicting CORS headers. Duplicate CORS headers can cause browser errors or unexpected behavior.

Testing CORS Configuration

Validate your CORS configuration with both automated tests and manual curl verification:

# Test preflight request
curl -X OPTIONS 'https://api.soninow.com/v1/orders' \
  -H 'Origin: https://app.soninow.com' \
  -H 'Access-Control-Request-Method: POST' \
  -I

# Verify response includes correct headers
# Access-Control-Allow-Origin: https://app.soninow.com
# Access-Control-Allow-Methods: POST, GET, OPTIONS

# Test actual cross-origin request
curl 'https://api.soninow.com/v1/users/me' \
  -H 'Origin: https://app.soninow.com' \
  -H 'Authorization: Bearer test_token' \
  -I

# Test invalid origin is rejected
curl 'https://api.soninow.com/v1/users/me' \
  -H 'Origin: https://evil.com' \
  -I
# Should NOT include Access-Control-Allow-Origin header
// Browser-based CORS test
async function testCors() {
  try {
    const response = await fetch('https://api.soninow.com/v1/status', {
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
    });
    console.log('CORS headers:', {
      allowOrigin: response.headers.get('access-control-allow-origin'),
      allowCredentials: response.headers.get('access-control-allow-credentials'),
    });
  } catch (error) {
    console.error('CORS error:', error);
  }
}

Common CORS Pitfalls

  • Using Access-Control-Allow-Origin: * with credentials — the browser rejects this combination
  • Echoing the Origin header back without validation — allows any site
  • Missing Vary: Origin on cached responses — serves wrong-origin responses from cache
  • Overly permissive allowed methods — exposing DELETE when only GET is needed
  • Exposing too many response headers via Access-Control-Expose-Headers

Correct CORS configuration enables secure cross-origin communication without exposing your API to unauthorized consumers. Our <a href="/services/web-development">web development team</a> implements CORS policies tailored to each application's architecture. Contact SoniNow to review your CORS configuration.