Site icon GetPageSpeed

NGINX If Is Evil: Complete Guide to Safe Conditional Logic

NGINX If Is Evil: Complete Guide to Safe Conditional Logic

The phrase “NGINX if is evil” has become legendary in the web server community. If you’ve configured NGINX for any significant project, you’ve likely encountered this warning. But what makes the if directive so dangerous? Is this an exaggeration, or is there real technical substance behind this notorious reputation?

This comprehensive guide explains exactly why the if directive causes problems in certain contexts, which use cases are genuinely safe, and how to implement robust conditional logic without falling into the traps that have burned countless system administrators.

Understanding Why the If Directive Causes Problems

The if directive in NGINX behaves fundamentally differently from conditionals in programming languages. When NGINX developers added conditional logic support, they created something that looks like a simple if-else statement but operates through an entirely different mechanism. This architectural mismatch is precisely why the NGINX if is evil warning exists.

The Root Cause: Configuration Context Switching

When you write an if block in NGINX, you’re not simply adding a conditional check. You’re creating an entirely new configuration context. Here’s what happens internally:

  1. NGINX creates a fresh location configuration for every HTTP module
  2. The if block is treated as an implicit nested location
  3. When the condition evaluates to true, NGINX switches the entire request configuration to this new context
  4. Directives that weren’t explicitly set inside the if block don’t inherit from the parent location

This architectural decision means that some directives work perfectly inside if blocks while others fail in unexpected and sometimes spectacular ways. Understanding this mechanism is essential for avoiding the dangerous patterns.

The NGINX Rewrite Module Architecture

The if directive is part of the ngx_http_rewrite_module, which processes directives imperatively during the rewrite phase. However, most other NGINX directives are declarative and expect to operate within a stable configuration context.

This impedance mismatch between the imperative rewrite module and the declarative nature of other modules is the fundamental source of problematic behavior. When you understand this, you can predict which configurations will work and which will cause problems.

Safe Directives Inside NGINX If Blocks

Not all uses of if are problematic. The following directives are explicitly designed to work safely within if blocks:

1. The return Directive

Using return inside an if block is completely safe and is the most common pattern:

server {
    listen 80;
    server_name example.com;

    location / {
        # Safe: Block POST requests
        if ($request_method = POST) {
            return 405;
        }

        # Safe: Redirect HTTP to HTTPS
        if ($scheme = http) {
            return 301 https://$server_name$request_uri;
        }

        root /var/www/html;
    }
}

The return directive immediately terminates request processing and sends a response to the client, avoiding any configuration context issues.

2. The rewrite Directive

The rewrite directive is another safe choice, especially with the last or break flags:

server {
    listen 80;
    server_name example.com;

    location / {
        # Safe: Rewrite based on query parameter
        if ($arg_redirect) {
            rewrite ^ /new-location last;
        }

        # Safe: Rewrite old URLs to new format
        if ($request_uri ~* "^/old-path/(.*)$") {
            rewrite ^ /new-path/$1 permanent;
        }

        root /var/www/html;
    }
}

3. The set Directive

Setting variables inside if blocks works reliably and is useful for building complex conditional logic:

server {
    listen 80;
    server_name example.com;

    location / {
        set $is_mobile "";

        # Safe: Detect mobile user agents
        if ($http_user_agent ~* "(mobile|android|iphone|ipad)") {
            set $is_mobile "1";
        }

        # Use the variable later in your configuration
        add_header X-Mobile $is_mobile;
        root /var/www/html;
    }
}

Note: While set is generally safe, tools like Gixy take a conservative approach and flag it as potentially unsafe. In practice, set works correctly inside if blocks, but be aware that static analyzers may warn about it.

4. The break Directive

The break directive stops rewrite processing and is safe to use:

location / {
    if ($slow) {
        limit_rate 10k;
        break;
    }
}

Dangerous Patterns: What to Avoid

Understanding unsafe patterns is crucial for avoiding configuration nightmares. Here are documented problematic scenarios that demonstrate why NGINX if is evil in specific contexts:

Problem 1: Multiple If Blocks with add_header

This is a classic problematic example that still affects modern NGINX versions (tested on NGINX 1.26.3):

# DANGEROUS: Only X-Second will be present in the response!
location /api/ {
    set $condition 1;

    if ($condition) {
        add_header X-First "1";
    }

    if ($condition) {
        add_header X-Second "1";
    }

    return 204;
}

Testing this configuration reveals the problem:

curl -I http://localhost/api/
# HTTP/1.1 204 No Content
# X-Second: 1
# Note: X-First is MISSING!

Each if block creates a new configuration context, and the last matching if block wins for directives that can only be applied once. The add_header from the first if block is lost.

Problem 2: proxy_pass with Dynamic Components

When an if block is present, proxy_pass may behave unexpectedly with URI transformations. While tuning proxy_buffer_size is straightforward, combining proxy_pass with if blocks requires extra caution:

# RISKY: The /v2/ URI transformation might not work as expected
location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "*";
        return 204;
    }

    proxy_pass http://backend/v2/;
}

Problem 3: try_files Interaction

Combining if with try_files has historically caused issues. As explained in our article about try_files performance implications, the try_files directive itself has quirks, and adding if blocks compounds the complexity:

# POTENTIALLY PROBLEMATIC
location / {
    set $check 1;

    if ($check) {
        # Even an empty if block can affect try_files behavior
    }

    try_files $uri $uri/ @fallback;
}

While modern NGINX versions (1.26+) have improved this behavior, mixing if with try_files should still be approached with caution.

Automated Detection with Gixy

Gixy is a static analyzer for NGINX configurations that automatically detects security misconfigurations, including problematic if usage. It’s an excellent tool for catching “if is evil” patterns before they cause problems in production.

Installing Gixy

On RHEL-based systems (Rocky Linux, AlmaLinux, CentOS), install Gixy from the GetPageSpeed repository:

dnf install https://extras.getpagespeed.com/release-latest.rpm
dnf install gixy

Running Gixy

Analyze your NGINX configuration:

gixy /etc/nginx/nginx.conf

Gixy will report issues like:

>> Problem: [if_is_evil] If is Evil... when used in location context.
Severity: HIGH
Description: Directive "if" has problems when used in location context,
in some cases it does not do what you expect but something completely
different instead.
Additional info: https://gixy.org/en/checks/if-is-evil/

Gixy checks for multiple security issues including SSRF vulnerabilities, HTTP splitting attacks, and header redefinition problems—many of which can be inadvertently introduced through improper if usage.

Better Alternatives to the If Directive

The map Directive: Your Best Friend

The map directive is the recommended alternative for most conditional logic. It’s evaluated during variable initialization, not during request processing, making it predictable and efficient:

# Define the map at http level
map $request_method $method_not_allowed {
    default 0;
    POST    1;
    PUT     1;
    DELETE  1;
}

server {
    listen 80;
    server_name example.com;

    location / {
        if ($method_not_allowed) {
            return 405 "Method not allowed";
        }

        root /var/www/html;
    }
}

The map directive supports complex pattern matching with regular expressions:

# Route based on user agent
map $http_user_agent $backend {
    default         "http://desktop-backend";
    ~*mobile        "http://mobile-backend";
    ~*googlebot     "http://seo-backend";
}

server {
    location / {
        proxy_pass $backend;
    }
}

Using map effectively avoids most scenarios where the if directive causes problems.

Multiple Conditions Using Variable Concatenation

NGINX doesn’t support native AND/OR operators in if statements. Use variable concatenation instead:

server {
    listen 80;
    server_name example.com;

    location / {
        set $condition "";

        if ($request_method = POST) {
            set $condition "P";
        }

        if ($http_x_api_key = "secret") {
            set $condition "${condition}K";
        }

        # Only matches when BOTH conditions are true
        if ($condition = "PK") {
            return 200 "Authenticated POST request";
        }

        return 403 "Access denied";
    }
}

The error_page Named Location Trick

For complex routing that would typically require unsafe if usage, the error_page pattern provides a clean solution:

server {
    listen 80;
    server_name example.com;

    location /api/ {
        # Trigger named location for OPTIONS requests
        error_page 418 = @cors_preflight;

        if ($request_method = OPTIONS) {
            return 418;
        }

        # This proxy_pass works correctly
        proxy_pass http://backend/v2/;
    }

    location @cors_preflight {
        add_header Access-Control-Allow-Origin "*";
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Max-Age 86400;
        return 204;
    }
}

This pattern uses HTTP status code 418 (I’m a teapot) as an internal routing mechanism. The error_page directive catches it and routes to the named location, avoiding the configuration context issues entirely.

Practical Examples: Real-World Safe Patterns

HTTP to HTTPS Redirect

server {
    listen 80;
    server_name example.com www.example.com;

    # Safe and efficient - no if needed
    return 301 https://example.com$request_uri;
}

For conditional redirect based on the specific domain:

server {
    listen 80;
    server_name example.com www.example.com;

    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }

    return 301 https://$host$request_uri;
}

Maintenance Mode

# Using map for maintenance bypass (e.g., admin IPs)
map $remote_addr $maintenance_bypass {
    default         0;
    192.168.1.100   1;  # Admin IP
    10.0.0.0/8      1;  # Internal network
}

server {
    listen 80;
    server_name example.com;

    set $maintenance 1;  # Set to 0 to disable maintenance mode

    if ($maintenance_bypass) {
        set $maintenance 0;
    }

    if ($maintenance = 1) {
        return 503 "Site under maintenance";
    }

    root /var/www/html;
}

Bot Detection and Blocking

map $http_user_agent $bad_bot {
    default         0;
    ~*crawl         0;  # Allow legitimate crawlers
    ~*googlebot     0;
    ~*bingbot       0;
    ~*wget          1;  # Block wget
    ~*python        1;  # Block Python scripts
    ""              1;  # Block empty user agents
}

server {
    listen 80;
    server_name example.com;

    if ($bad_bot) {
        return 403 "Access denied";
    }

    root /var/www/html;
}

Geographic Restrictions

# Requires ngx_http_geoip2_module
map $geoip2_country_code $allowed_country {
    default no;
    US      yes;
    CA      yes;
    GB      yes;
}

server {
    listen 80;
    server_name example.com;

    if ($allowed_country = no) {
        return 403 "Service not available in your region";
    }

    root /var/www/html;
}

Testing Your If Configurations

Always validate your configuration before deploying. The fact that the NGINX if is evil in certain contexts doesn’t mean you can’t use it—it means you must test thoroughly:

# Test configuration syntax
nginx -t

# Run static analysis with Gixy
gixy /etc/nginx/nginx.conf

# Test with specific HTTP methods
curl -I http://localhost/your-path
curl -X POST http://localhost/your-path
curl -X OPTIONS http://localhost/your-path

# Test with custom headers
curl -H "User-Agent: Mobile" http://localhost/your-path
curl -H "X-Special: true" http://localhost/your-path

# Check response headers to verify behavior
curl -sI http://localhost/ | grep -E "^X-|^HTTP"

Historical Context and Modern Improvements

The original “if is evil” warning from the NGINX wiki was written years ago when these problems were more severe and less understood. Modern NGINX versions (1.20+) have improved some edge cases, but the fundamental architecture hasn’t changed.

The core issues that make certain if patterns problematic remain:

What has improved is documentation and community knowledge. You now have clear guidance on safe versus unsafe patterns, and tools like Gixy can automatically detect problematic configurations.

Summary: The If Directive Rules

  1. Safe directives inside if blocks: return, rewrite, set, break
  2. Avoid inside if blocks: add_header (with multiple if blocks), proxy_pass with URI transformations, try_files
  3. Use map directive for complex conditional logic—it’s almost always better
  4. Use named locations with error_page for routing that would require unsafe if patterns
  5. Test thoroughly before deploying any configuration with if blocks
  6. Use Gixy to automatically detect problematic patterns
  7. Understand the context that makes certain patterns dangerous: configuration switching, not the directive itself

The if directive isn’t inherently evil—it’s simply misunderstood. By understanding how NGINX processes if blocks and using the safe patterns documented here, you can write robust conditional configurations without encountering problems.

Remember: when in doubt, reach for the map directive. It handles most conditional logic scenarios elegantly and avoids the pitfalls that gave rise to the infamous “NGINX if is evil” warning.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version