Why "If Is Evil"
The if directive in NGINX is not a general-purpose conditional statement like in programming languages. It's part of the rewrite module and behaves in non-intuitive ways that can cause serious problems:
The Problems with if
- Creates a pseudo-location - if blocks create new configuration contexts
- Directive inheritance is broken - Some directives don't work inside if
- Can cause segfaults - Certain combinations crash NGINX
- Evaluation timing is confusing - Executes at unexpected points
- Only certain directives are safe - Most directives cause problems
The Technical Problem
When NGINX processes a request, it goes through multiple phases. The if directive evaluates during the rewrite phase, but creates a nested location that affects content handling phases. This mismatch causes unexpected behavior.
# This looks logical but DOESN'T work as expected
location / {
set $true 1;
if ($true) {
# This block creates a new pseudo-location
# Some directives here won't work!
}
# Directives here might not execute
# because the if block "captured" the request
}
Dangerous if Patterns
Pattern 1: if with proxy_pass
# proxy_pass inside if is BROKEN
location / {
if ($request_uri ~ ^/api/) {
proxy_pass http://api-backend;
}
proxy_pass http://default-backend;
}
# Result: Unpredictable behavior, wrong backend, or crashes
Pattern 2: if with try_files
# try_files doesn't work inside if
location / {
if (-f $request_filename) {
# This seems logical...
}
try_files $uri $uri/ /index.html;
}
# Result: try_files may be ignored entirely
Pattern 3: Setting Headers in if
# Headers might not be set
location / {
if ($request_method = POST) {
add_header X-Post-Request true;
}
proxy_pass http://backend;
}
# Result: Header may or may not appear depending on other factors
Pattern 4: Multiple if Blocks
# Multiple ifs interact in unpredictable ways
location / {
set $backend "default";
if ($http_x_custom) {
set $backend "custom";
}
if ($arg_override) {
set $backend "override";
}
# Which backend? It's complicated...
}
When if IS Safe
There are exactly two safe uses of if inside a location:
1. return (immediate response)
# SAFE: return terminates request processing
location / {
if ($request_method = OPTIONS) {
return 204;
}
if ($http_user_agent ~* "badbot") {
return 403;
}
proxy_pass http://backend;
}
2. rewrite ... last
# SAFE: rewrite last exits the location
location / {
if ($arg_redirect) {
rewrite ^ /redirected last;
}
# Normal processing continues if no redirect
}
if with directives that terminate request processing in that location: return and rewrite ... last.
Safe Alternatives
Use map Instead
# Define logic with map (in http block)
map $request_uri $backend {
default http://default-backend;
~^/api/ http://api-backend;
~^/admin/ http://admin-backend;
}
# Use the variable in location
server {
location / {
proxy_pass $backend;
}
}
Use Multiple Locations
# Instead of if, use separate locations
location /api/ {
proxy_pass http://api-backend;
}
location /admin/ {
proxy_pass http://admin-backend;
}
location / {
proxy_pass http://default-backend;
}
Use try_files for File Checks
# Instead of if (-f $request_filename)
location / {
try_files $uri $uri/ @fallback;
}
location @fallback {
proxy_pass http://backend;
}
Use error_page for Conditional Routing
# Route based on file existence
location / {
try_files $uri $uri/ =404;
error_page 404 = @backend;
}
location @backend {
proxy_pass http://backend;
}
Use geo for IP-Based Decisions
# Instead of if checking $remote_addr
geo $is_internal {
default 0;
192.168.0.0/16 1;
10.0.0.0/8 1;
}
map $is_internal $internal_backend {
1 http://internal-backend;
0 http://public-backend;
}
server {
location / {
proxy_pass $internal_backend;
}
}
Common Refactoring Examples
Example 1: Blocking User Agents
# BAD: if for blocking
location / {
if ($http_user_agent ~* "badbot|scraper") {
return 403;
}
proxy_pass http://backend;
}
# GOOD: map + return in separate location
map $http_user_agent $bad_bot {
default 0;
~*badbot 1;
~*scraper 1;
}
server {
if ($bad_bot) {
return 403;
}
# Note: This if is in server context, which is safer
# Or use a dedicated location:
location / {
proxy_pass http://backend;
}
}
Example 2: Maintenance Mode
# BAD: file check with if
location / {
if (-f /etc/nginx/maintenance.flag) {
return 503;
}
proxy_pass http://backend;
}
# GOOD: Use a variable set externally
# In nginx.conf or via include:
set $maintenance 0; # Set to 1 to enable
server {
if ($maintenance) {
return 503;
}
location / {
proxy_pass http://backend;
}
}
Detecting with Gixy
Gixy detects dangerous uses of the if directive:
$ gixy /etc/nginx/nginx.conf
==================== Results ====================
[if_is_evil] If is evil... when used in location context.
Severity: HIGH
Description: Using "if" in location context can cause unexpected
behavior or segfaults.
Reason: Directive "proxy_pass" may not work as expected inside if.
Pseudo config:
location / {
if ($arg_backend) {
proxy_pass http://backend;
}
}
File: /etc/nginx/conf.d/site.conf
Line: 12
==================== Summary ====================
Total issues: 1 (High: 1)
Quick Reference
| Avoid | Use Instead |
|---|---|
if + proxy_pass |
map + variable in proxy_pass |
if + add_header |
map + always clause |
if (-f ...) |
try_files |
if ($uri ...) |
Multiple location blocks |
if ($remote_addr ...) |
geo block |