NGINX Origin & Referer Validation

Properly validate Origin and Referer headers to prevent CORS bypass, CSRF, and hotlinking attacks in NGINX.

Updated: January 2025 * 8 min read

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

Further Reading