Why Header Validation Matters
Origin and Referer headers help verify where requests come from. Weak validation patterns can allow attackers to bypass your security controls, leading to:
- CORS bypass - Cross-origin requests from unauthorized domains
- CSRF attacks - Malicious requests appearing to come from your site
- Hotlink abuse - Bandwidth theft by embedding your resources
- Data exfiltration - Stealing data through cross-origin requests
Common Validation Mistakes
1. Unescaped Dots in Regex
# Dot matches ANY character, not just literal dot
valid_referers server_names ~example.com;
# Matches: example.com (intended)
# Also matches: exampleXcom.attacker.com (bypass!)
2. Missing Anchors
# No anchors - matches anywhere in string
if ($http_origin ~* "example.com") {
add_header Access-Control-Allow-Origin $http_origin;
}
# Matches: https://example.com (intended)
# Also matches: https://example.com.attacker.com (bypass!)
# Also matches: https://attacker.com/example.com (bypass!)
3. Allowing "none" in valid_referers
# "none" allows requests with no Referer
valid_referers none server_names *.example.com;
if ($invalid_referer) {
return 403;
}
# Attacker can simply omit the Referer header
# Many privacy tools strip Referer automatically
4. Subdomain Wildcards Gone Wrong
# Missing proper domain boundary
valid_referers ~\.?example\.com;
# Matches: example.com, sub.example.com (intended)
# Also matches: notexample.com (bypass!)
# The \.? makes the dot optional
Secure Patterns
Proper valid_referers Usage
# Correct valid_referers with escaped dots and boundaries
valid_referers server_names
~^https?://([^/]*\.)?example\.com(/|$);
if ($invalid_referer) {
return 403;
}
# Only matches:
# - https://example.com
# - https://example.com/path
# - https://sub.example.com
# - http://any.sub.example.com/path
Proper Origin Validation
# Map with explicit allowed origins
map $http_origin $cors_origin {
default "";
"https://example.com" $http_origin;
"https://app.example.com" $http_origin;
"https://www.example.com" $http_origin;
}
server {
location /api/ {
if ($cors_origin = "") {
return 403;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
}
}
Regex Origin Validation
# If you need regex, anchor it properly
map $http_origin $cors_origin {
default "";
# Anchored regex with escaped dots
~^https://([a-z0-9-]+\.)?example\.com$ $http_origin;
}
# Only matches:
# - https://example.com
# - https://sub.example.com
# - https://sub-domain.example.com
# Does NOT match:
# - https://example.com.attacker.com
# - https://notexample.com
# - http://example.com (no http allowed)
Best Practice: Prefer explicit whitelist maps over regex patterns. They're easier to audit and less prone to bypass.
CORS Configuration Examples
Single Origin
# Simple single-origin CORS
location /api/ {
add_header Access-Control-Allow-Origin "https://example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
Multiple Origins with Validation
# Multiple allowed origins
map $http_origin $cors_header {
default "";
"https://app.example.com" "https://app.example.com";
"https://www.example.com" "https://www.example.com";
"https://admin.example.com" "https://admin.example.com";
}
server {
location /api/ {
# Only add header if origin is allowed
add_header Access-Control-Allow-Origin $cors_header always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials true always;
# Handle preflight
if ($request_method = OPTIONS) {
return 204;
}
# Reject unknown origins
if ($cors_header = "") {
return 403;
}
proxy_pass http://backend;
}
}
Hotlink Protection
# Protect images/media from hotlinking
location ~* \.(gif|jpe?g|png|webp|mp4)$ {
valid_referers none blocked server_names
~^https?://([^/]*\.)?example\.com(/|$);
if ($invalid_referer) {
return 403;
}
# Or redirect to placeholder
# if ($invalid_referer) {
# rewrite ^ /hotlink-placeholder.png last;
# }
}
# Note: "none" and "blocked" allow direct access and
# requests where Referer was stripped. Remove if too permissive.
Referer Can Be Omitted
Remember that browsers may not send Referer headers when:
- User has privacy settings enabled
- Request is from HTTPS to HTTP (downgrade)
- Referrer-Policy header restricts it
- User is using privacy browser extensions
Detecting with Gixy
Gixy detects weak Origin and Referer validation:
$ gixy /etc/nginx/nginx.conf
==================== Results ====================
[origins] Validation regex for "origin" matches untrusted domain.
Severity: HIGH
Description: CORS origin/referer validation regex can be bypassed
or matches invalid values.
Reason: Regex "example.com" matches "exampleXcom.evil.com"
Pseudo config:
location /api/ {
if ($http_origin ~* "example.com") {
add_header Access-Control-Allow-Origin $http_origin;
}
}
File: /etc/nginx/conf.d/api.conf
Line: 12
[valid_referers] Used "none" as valid referer.
Severity: HIGH
Description: Never trust undefined referer; using "none" allows
requests with missing referer header.
File: /etc/nginx/conf.d/images.conf
Line: 5
==================== Summary ====================
Total issues: 2 (High: 2)
Quick Reference: Regex Escaping
| Character | Meaning | To Match Literally |
|---|---|---|
. |
Any character | \. |
* |
Zero or more | \* |
+ |
One or more | \+ |
? |
Optional | \? |
$ |
End of string | \$ |
Validation Checklist
- Escape all dots in domain patterns (
\.) - Use anchors (
^and$) to prevent prefix/suffix attacks - Prefer explicit whitelists over regex
- Consider whether to allow "none" referer carefully
- Test patterns against bypass attempts
- Use map directive for clean, auditable CORS configs
- Run Gixy to catch common mistakes