Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
For most use cases with authentication/cookies, specify a single trusted origin:
when HTTP_REQUEST priority 200 {
unset -nocomplain cors_origin
# Validate against single trusted origin
if { [HTTP::header Origin] equals "https://example.com" } {
if { ([HTTP::method] equals "OPTIONS") and
([HTTP::header exists "Access-Control-Request-Method"]) } {
# Handle preflight request
HTTP::respond 200 "Access-Control-Allow-Origin" "https://example.com" \
"Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
"Access-Control-Allow-Headers" "Content-Type, Authorization" \
"Access-Control-Allow-Credentials" "true" \
"Access-Control-Max-Age" "86400" \
"Vary" "Origin"
return
} else {
set cors_origin "https://example.com"
}
}
}
when HTTP_RESPONSE {
if { [info exists cors_origin] } {
HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
HTTP::header insert "Access-Control-Allow-Credentials" "true"
HTTP::header insert "Vary" "Origin"
}
}
Note: When using credentials, you must not use wildcards (*) for either Access-Control-Allow-Origin or Access-Control-Allow-Headers. Specific values must be provided.
To allow multiple specific origins from the same domain family:
when HTTP_REQUEST priority 200 {
unset -nocomplain cors_origin
# Validate against multiple trusted origins
if { [HTTP::header Origin] ends_with ".example.com" or
[HTTP::header Origin] equals "https://example.com" } {
if { ([HTTP::method] equals "OPTIONS") and
([HTTP::header exists "Access-Control-Request-Method"]) } {
# Handle preflight - echo back the validated origin
HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
"Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
"Access-Control-Allow-Headers" "Content-Type, Authorization, X-Requested-With" \
"Access-Control-Allow-Credentials" "true" \
"Access-Control-Max-Age" "86400" \
"Vary" "Origin"
return
} else {
set cors_origin [HTTP::header "Origin"]
}
}
}
when HTTP_RESPONSE {
if { [info exists cors_origin] } {
HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
HTTP::header insert "Access-Control-Allow-Credentials" "true"
HTTP::header insert "Vary" "Origin"
}
}
For strict control with a defined list of allowed origins:
when HTTP_REQUEST priority 200 {
unset -nocomplain cors_origin
# Define allowed origins
set allowed_origins {
"https://app.example.com"
"https://dashboard.example.com"
"https://mobile.example.com"
}
set origin [HTTP::header Origin]
if { [lsearch -exact $allowed_origins $origin] >= 0 } {
if { ([HTTP::method] equals "OPTIONS") and
([HTTP::header exists "Access-Control-Request-Method"]) } {
HTTP::respond 200 "Access-Control-Allow-Origin" $origin \
"Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
"Access-Control-Allow-Headers" "Content-Type, Authorization" \
"Access-Control-Allow-Credentials" "true" \
"Access-Control-Max-Age" "86400" \
"Vary" "Origin"
return
} else {
set cors_origin $origin
}
}
}
when HTTP_RESPONSE {
if { [info exists cors_origin] } {
HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
HTTP::header insert "Access-Control-Allow-Credentials" "true"
HTTP::header insert "Vary" "Origin"
}
}
Only use for completely public resources without authentication. This allows any website to access your API:
when HTTP_REQUEST {
if { ([HTTP::method] equals "OPTIONS") and
([HTTP::header exists "Access-Control-Request-Method"]) } {
# Handle preflight for public API
HTTP::respond 200 "Access-Control-Allow-Origin" "*" \
"Access-Control-Allow-Methods" "GET, POST, OPTIONS" \
"Access-Control-Allow-Headers" "Content-Type" \
"Access-Control-Max-Age" "86400"
return
}
}
when HTTP_RESPONSE {
# Add CORS headers to all responses
HTTP::header insert "Access-Control-Allow-Origin" "*"
}
Important: When using wildcard (*) for Access-Control-Allow-Origin, you cannot include Access-Control-Allow-Credentials: true. This is a CORS specification requirement.
Apply different CORS policies based on URI paths:
when HTTP_REQUEST priority 200 {
unset -nocomplain cors_origin
set uri [HTTP::uri]
# Public API - no credentials
if { $uri starts_with "/api/public" } {
if { [HTTP::method] equals "OPTIONS" } {
HTTP::respond 200 "Access-Control-Allow-Origin" "*" \
"Access-Control-Allow-Methods" "GET, POST" \
"Access-Control-Allow-Headers" "Content-Type"
return
}
set cors_origin "*"
# Private API - with credentials
} elseif { $uri starts_with "/api/private" } {
if { [HTTP::header Origin] ends_with ".example.com" } {
if { [HTTP::method] equals "OPTIONS" } {
HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
"Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
"Access-Control-Allow-Headers" "Content-Type, Authorization" \
"Access-Control-Allow-Credentials" "true" \
"Vary" "Origin"
return
}
set cors_origin [HTTP::header "Origin"]
set cors_credentials "true"
}
}
}
when HTTP_RESPONSE {
if { [info exists cors_origin] } {
HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
if { [info exists cors_credentials] } {
HTTP::header insert "Access-Control-Allow-Credentials" $cors_credentials
HTTP::header insert "Vary" "Origin"
}
}
}
F5 iRules typically handle CORS using two events:
This pattern ensures CORS headers are added consistently while allowing the backend to process non-preflight requests normally.
Origin header without validationAccess-Control-Allow-Credentials: true):
*) for Access-Control-Allow-Origin*) for Access-Control-Allow-HeadersVary: Origin header when origin varies to prevent cache poisoningends_with, equals) rather than wildcards for domain validationAccess-Control-Max-Age to reduce preflight requests (86400 = 24 hours)To apply these iRules:
For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.
The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.
Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain