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-Originmust be an explicit origin (not*)Access-Control-Allow-Credentials: truemust be setAccess-Control-Allow-Originin 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
Originheader back without validation — allows any site - Missing
Vary: Originon 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.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

Authentication Patterns in Modern Web Apps: JWT, OAuth, and Session Management
A guide to authentication patterns for web applications including JWT implementation, OAuth 2.0 flows, refresh tokens, session management, and secure storage.

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys
A guide to modern authentication patterns comparing JWT, session-based auth, and passkeys including implementation strategies, security considerations, and user experience.