Skip to content

Variables in Non-Expanding Directives

Gixy Check ID: non_expanding_variable

Many NGINX directives expand variables, so it is easy to assume they all do. map values can contain variables. geo and split_clients values cannot: NGINX stores $binary_remote_addr as the literal 19-character string $binary_remote_addr. nginx -t reports no warning.

The shared rate-limit bucket

This configuration looks like a per-client rate limit with an allowlist:

geo $limit_key {
    default   $binary_remote_addr;   # stored literally!
    127.0.0.1 "";
}
limit_req_zone $limit_key zone=dynamic:10m rate=5r/s;

Every client outside the allowlist gets the same key, $binary_remote_addr, so they all share one bucket. One crawler burst exhausts the limit and real visitors get 503 responses. Gixy reports this as HIGH and names the limit_req_zone or limit_conn_zone directive that uses the key.

Safer alternative

Let geo classify clients and let a chained map expand the variable. This is the allowlisting pattern from the NGINX rate limiting guide:

geo $limit {
    default   1;
    127.0.0.1 0;
    10.0.0.0/8 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=dynamic:10m rate=5r/s;

Use the same approach for split_clients: return a literal marker, then map it to a variable value.

Matches that are never expanded

location, server_name, rewrite and if patterns are compiled once, when NGINX loads the configuration, so variables in them are never substituted. Gixy reports these as MEDIUM:

set $admin admin;

# Prefix location: only matches the literal URI "/$admin/"
location /$admin/ { deny all; }

# Regex: "$" is the end-of-string anchor, so "^/$admin/" can never match
# and the deny is silently skipped
location ~ ^/$admin/ { deny all; }

rewrite ^/$old/(.*)$ /new/$1 permanent;   # never fires
if ($uri ~ $blocked) { return 403; }      # regex is the text "$blocked"
server_name $tenant.example.com;          # matches Host "$tenant.example.com"

Write the value out, or move the comparison into a map:

location ~ ^/admin/ { deny all; }

map $uri $blocked_uri {
    default         0;
    ~^/private      1;
}

A dollar sign escaped as \$ or inside a character class such as [$] is a literal character and is not reported. server_name $hostname is a documented special name and is accepted.

auth_request URI

auth_request sends its subrequest to the URI exactly as written. With auth_request /auth/$host; the subrequest goes to /auth/$host. If any location answers that URI with a 2xx status, every request is authorized, so Gixy reports this as HIGH. Use a fixed URI and pass request data to the auth location in headers:

location / {
    auth_request /auth;
}

location = /auth {
    internal;
    proxy_set_header X-Original-Host $host;
    proxy_pass http://auth-backend;
}

Unrendered environment placeholders

NGINX does not read environment variables in its configuration. A ${UPPER_CASE} name that no set, map or other directive defines is almost always a template placeholder that was never rendered:

# Copied from a Docker template without running envsubst
proxy_pass http://${BACKEND_HOST}:8080;

Gixy reports these as MEDIUM. Render the template with envsubst before NGINX starts. The official Docker image does this for /etc/nginx/templates/*.template.

Paths that do not support variables

These directives take their path literally and create files or directories with $ in the name. Gixy reports them as LOW:

Directive Literal argument
error_log file path
client_body_temp_path, proxy_temp_path, fastcgi_temp_path, uwsgi_temp_path, scgi_temp_path directory
proxy_cache_path, fastcgi_cache_path, uwsgi_cache_path, scgi_cache_path directory
# Creates a file literally named "$host.error.log"
error_log /var/log/nginx/$host.error.log;

access_log does support variables, so it is not reported. Variables are also expanded in map values, add_header, proxy_set_header, root, alias, try_files, proxy_pass and the limit_req_zone key.

Catch this before it reaches production

GetPageSpeed Amplify runs Gixy on a schedule across every NGINX host and alerts you on new findings and CVEs, alongside runtime metrics. Set up continuous monitoring.