ChronoPay-Backend implements production-grade CORS (Cross-Origin Resource Sharing) configuration with secure allowlist-based validation. This replaces the permissive default cors() middleware with a configurable, security-focused solution.
- Secure by default: No origins are allowed until explicitly configured
- Allowlist validation: Only configured origins can access the API
- Wildcard pattern support: Support for subdomain patterns (e.g.,
https://*.example.com) - Environment-based configuration: Different configs for development, staging, and production
- Credentials support: Configurable credential handling for cross-origin requests
- Comprehensive validation: Origin validation with security constraints
- Production-tested: Designed for enterprise deployments
CORS configuration is loaded from environment variables:
| Variable | Description | Default | Example |
|---|---|---|---|
CORS_ALLOWED_ORIGINS |
Comma-separated list of allowed origins | See below | https://example.com, https://*.example.com |
CORS_ALLOWED_METHODS |
Comma-separated HTTP methods | GET,POST,PUT,DELETE,PATCH,OPTIONS |
GET, POST |
CORS_ALLOWED_HEADERS |
Comma-separated request headers | Content-Type,Authorization |
Content-Type, X-Custom-Header |
CORS_ALLOW_CREDENTIALS |
Allow credentials (cookies, etc.) | true |
false |
CORS_MAX_AGE |
Preflight cache duration (seconds) | 86400 |
3600 |
NODE_ENV |
Environment (development/staging/production) | development |
production |
Development/Staging (NODE_ENV != "production"):
- http://localhost:3000
- http://localhost:3001
Production:
- Empty by default (must be explicitly configured via environment variables)
# Uses defaults - no configuration needed
NODE_ENV=developmentNODE_ENV=production
CORS_ALLOWED_ORIGINS=https://app.chronopay.com,https://*.chronopay.com
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=86400NODE_ENV=staging
CORS_ALLOWED_ORIGINS=https://staging.chronopay.com,https://localhost:3000
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Request-IDExact origin strings are matched as-is:
https://example.commatches exactlyhttps://example.com- Does NOT match
https://app.example.comorhttps://example.com:8443
Wildcard patterns support dynamic subdomains:
https://*.example.commatcheshttps://app.example.comandhttps://sub.app.example.com- Does NOT match
https://example.com(base domain is not matched) - Does NOT match
https://example.org(top-level domain is locked)
- Wildcard-only patterns (
*) are rejected for security - Multiple wildcards per pattern are rejected
- Wildcards must be followed by a dot (prevents
*.comstyle matches) - All origins must be valid URLs (scheme + domain at minimum)
- Port numbers must match exactly
-
Origin Header Trust: The implementation assumes the
Originheader is trustworthy. In practice, browsers always send this for CORS requests, but server-side validation is still essential. -
HTTPS Production: Production deployments should use HTTPS schemes only. Avoid
http://URLs in production. -
Credential Isolation: When
CORS_ALLOW_CREDENTIALS=true, theAccess-Control-Allow-Originheader is set to the specific origin (not*), preventing credential leakage. -
Preflight Caching: The
maxAgesetting controls how long browsers cache preflight results. Higher values reduce requests but prevent quick updates.
- When
CORS_ALLOWED_ORIGINSis empty in production, no origins are allowed - Result: All CORS requests are rejected
- Mitigation: Explicitly configure allowed origins in production
- Origins that don't parse as valid URLs are rejected
- Result: Request processed without CORS headers
- Mitigation: None needed - feature is working correctly
- Origins that don't match any pattern in the allowlist are rejected
- Result: Request processed without CORS headers
- Mitigation: Verify origin list matches your deployment domains
- Invalid wildcard patterns (e.g.,
*.com,*.*.example.com) are caught during validation - Result: Server startup fails with clear error message
- Mitigation: Use proper wildcard syntax:
https://*.example.com
The getCORSConfig() function:
- Detects the NODE_ENV environment variable
- Loads production-specific defaults for
production - Loads development defaults otherwise
- Overrides with environment variables if provided
- Parses CSV lists and boolean/numeric values
The isOriginAllowed() function:
- Validates origin exists and is a non-empty string
- Validates origin is a well-formed URL
- Checks for exact match in allowlist
- Checks for wildcard pattern match
- Returns boolean result
The createCORSMiddleware() function:
- Checks if origin is in allowlist
- For allowed origins: Sets all CORS headers
- For disallowed origins: Only processes request without CORS headers
- Handles OPTIONS (preflight) requests specially
- Returns 403 for disallowed preflight requests
Comprehensive test coverage includes:
- Origin validation (exact matches, patterns, wildcards)
- Configuration loading from environment variables
- Preflight request handling
- Credentials handling
- Edge cases (empty lists, invalid URLs, missing headers)
- Security constraint validation
Run tests with:
npm test -- src/__tests__/cors.test.tsTarget coverage: ≥95%
Loads CORS configuration from environment variables and NODE_ENV.
Returns: CORSConfig object with loaded configuration
Validates CORS configuration and throws errors for invalid configs.
Parameters:
config: CORSConfig object to validate
Returns: true if valid
Throws: Error if configuration is invalid
Checks if an origin is allowed based on the allowlist.
Parameters:
origin: Origin header from requestallowedOrigins: List of allowed origins and patterns
Returns: true if allowed, false otherwise
Creates an Express middleware for CORS validation.
Parameters:
config: CORSConfig object fromgetCORSConfig()
Returns: Express middleware function
Behavior:
- Validates origin against allowlist
- Sets CORS headers for allowed origins
- Returns 403 for disallowed preflight requests
Symptom: "Access to XMLHttpRequest has been blocked by CORS policy"
Causes:
- Origin not in allowlist
- Invalid wildcard patterns
- Mismatched port numbers
- HTTP vs HTTPS mismatch
Solution: Check CORS_ALLOWED_ORIGINS and verify your domain matches exactly.
Symptom: OPTIONS request returns 403
Causes:
- Origin not in allowlist
- Preflight request sent without Origin header
Solution: Ensure origin is in allowlist and properly formatted.
Symptom: Origin is allowed unexpectedly or not allowed as expected
Causes:
- Environment variable not set
- Syntax error in CSV list (missing comma)
- Whitespace in origin strings
Solution: Verify environment variables are set correctly (check with echo $VARIABLE_NAME).
Symptom: "Invalid CORS configuration" error on startup
Causes:
- Wildcard-only pattern (
*) - Invalid URL in allowlist
- Invalid maxAge value
Solution: Check error message and review configuration.
When committing CORS-related changes:
feat(cors): implement allowlist configuration
- Add CORS configuration module with environment-based settings
- Create CORS middleware for origin validation
- Support wildcard patterns for flexible subdomain matching
- Replace permissive default cors() with secure allowlist validation
- Include 95%+ test coverage