×
Community Blog From an 18-Year-Old Hidden Nginx Vulnerability to the Evolution of Gateway Security Architecture

From an 18-Year-Old Hidden Nginx Vulnerability to the Evolution of Gateway Security Architecture

This article analyzes the root causes of the 18-year-old Nginx vulnerability (CVE-2026-42945) and explores the evolution of gateway security architecture.

By Cheng Tan

CVE-2026-42945, CVSS 9.2, affecting Nginx 0.6.27 to 1.30.0, is an 18-year-old heap overflow vulnerability. It is not an exquisite chain of exploit, but rather a most simple oversight of state management. But it is this very "rookie mistake" that gives us an opportunity to reexamine the security design philosophy of gateways.

Vulnerability Principle: The State Ghost Between Two-Pass Execution

Nginx's Script Engine

Nginx's rewrite and set directives are not simple string substitutions. They are compiled into a series of opcodes and executed by Nginx's internal script engine. This engine uses a classic performance optimization design—two-pass execution:

  • First Pass (Length Calculation): Traverses all opcodes to calculate the total length of the final string, and allocates a buffer of just the right size from the memory pool in one go.
  • Second Pass (Data Copy): Traverses again to write the actual data into the newly allocated buffer.

This design avoids repeated reallocations, which is a very reasonable optimization at the C language level. But it has an implicit prerequisite: the engine state seen by both passes must be completely identical.

The Fatal State Leak

Consider this extremely common Nginx configuration:

location ~ ^/api/(.*)$ {
    rewrite ^/api/(.*)$ /internal?migrated=true;
    set $original_endpoint $1;
}

The replacement string of rewrite contains a ?. When Nginx sees the ?, it assumes the subsequent part is a query string, so it calls ngx_http_script_start_args_code() and permanently sets the engine's e->is_args flag to 1.

Next, set $original_endpoint $1 is executed. This references the regex capture group $1, triggering ngx_http_script_complex_value_code(). Here comes the crucial part—in order to calculate the length of the variable value, this function creates a brand new, zero-initialized sub-enginele:

ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); // Completely zeroed out
le.ip = code->lengths->elts;

Because le.is_args is 0, the length calculation goes down the "do not escape" branch and returns the original length.

However, the copy phase uses the main enginee, whose is_args is still 1. Consequently, the copy code goes down the "needs escaping" branch, expanding characters like +, &, and = in the URI from 1 byte to 3 bytes (such as +%2B).

A buffer of raw_size is allocated, but raw_size + 2*N bytes of data are written. Heap overflow.

Why Wasn't It Discovered for 18 Years?

This question is more interesting than the vulnerability itself:

  1. Triggering requires three conditions to be met simultaneously: rewrite contains ?, set references a capture group, and the request URI contains a large number of characters that need to be escaped (+&=). If any is missing, no overflow occurs.
  2. Most overflows are silent: Nginx's memory pool is chunk-allocated. A small overflow is highly likely to fall into padding or the metadata of an adjacent chunk without immediately crashing.
  3. Worker crash does not equal vulnerability exposure: Under the multi-process architecture, after a worker crashes, the master automatically spawns a new worker, making it very hard for operators to notice whether an occasional segfault is an attack or a sporadic bug.
  4. Two-pass state consistency is not within the scope of audits: Static analysis tools check memory safety, and manual audits check logical correctness. However, an issue like "the implicit state contract between two execution phases being broken" is very hard for either to capture.

The implicit contract of the state machine was broken by a new feature, and this contract was never written down. When writing the rewrite engine in 2008, the semantics of is_args were "currently processing the query string part," which did not need to be reset once set—because the complex value logic would not be entered again within the same processing flow. Later, support for capture group references in the set directive broke this assumption.

Root Cause: Inherent Risks of Imperative Configuration

Nginx's rewrite, set, if, and other directives are essentially simulating an imperative programming language using a configuration language. It has variable assignment, regex capturing, conditional branches, loops (last/break), and even an implicit state machine.

This design is successful in terms of flexibility—you can implement almost any request processing logic using nginx.conf. But it also introduces fundamental problems:

The interaction effect between directives is unpredictable.rewrite changes the engine state, and set reads the modified state, with no documentation or mechanism to constrain this cross-directive state propagation. This is not unique to Nginx; any system trying to stuff programming capabilities into a configuration language will encounter it—it's just that here in Nginx, the consequence is RCE.

Envoy's Security Philosophy: Declarative Configuration

Envoy chose a completely different path. Its configuration is declarative:

route:
  match:
    regex: "^/api/(.*)$"
  rewrite:
    regex_rewrite:
      pattern:
        regex: "^/api/(.*)$"
      substitution: "/internal/\\1"

No variables, no assignments, no state machines. Each routing rule is independent and self-contained. The match and substitution of rewrite are completed in a single rule, eliminating the possibility of "first rewrite modifies the global state, then set reads the dirty state."

This design fundamentally eliminates the attack surface of state-leakage vulnerabilities. Envoy's route configuration does not need to maintain engine state across rules, so the two-pass inconsistency problem naturally does not exist.

However, declarative configuration also comes at a price—insufficient flexibility:

  • Capture groups can only be used within the substitution of the current rule and cannot be saved as variables to be passed to subsequent logic.
  • It cannot express "first rewrite the URI, then inject the capture group into the request header and pass it to the upstream."
  • The ability to manipulate query strings is limited—query_parameters is merely a match condition, not a rewriting tool.
  • There are no conditional branches and loops; complex request processing logic cannot be expressed.

For simple routing rewrites, Envoy is more than sufficient. But for complex configurations migrated from Nginx, especially those scenarios depending on rewrite + set + capture group passing, Envoy's native route configuration is inadequate.

Higress WASM Plugin: Replacing Directives with Code

Higress's WASM plugin mechanism provides an elegant solution—since imperative configuration has state management hazards and declarative configuration is not flexible enough, let's use real code to solve the problem.

Taking the equivalent capabilities of nginx rewrite + set as an example, the implementation of a Higress WASM plugin would look something like this:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
    // 1. Get request path
    path, _ := proxywasm.GetHttpRequestHeader(":path")
    pathPart, query := splitPathQuery(path)
    
    // 2. Regex matching
    for _, rule := range config.Rules {
        matches := rule.Regex.FindStringSubmatch(pathPart)
        if matches == nil {
            continue
        }
        
        // 3. Construct new path (replace capture groups)
        newPath := expandCaptures(rule.Replacement, matches)
        
        // 4. Handle query string
        newQuery := mergeQuery(query, rule.QueryAppend, rule.QueryTemplate, matches)
        
        // 5. Save variables (equivalent to nginx set)
        for _, v := range rule.SetVars {
            value := matches[v.CaptureGroup]
            // For subsequent plugins
            proxywasm.SetProperty([]string{v.Name}, []byte(value))
            // For upstream services
            proxywasm.AddHttpRequestHeader("X-Rewrite-"+v.Name, url.QueryEscape(value))
        }
        
        // 6. Write back modified path
        fullPath := joinPathQuery(newPath, newQuery)
        proxywasm.ReplaceHttpRequestHeader(":path", fullPath)
        
        if rule.Break {
            break
        }
    }
    return types.ActionContinue
}

What this code does is completely equivalent to Nginx's rewrite + set, but with several fundamental differences:

1. No Trap of Two-Pass State Inconsistency

Each time a request comes in, the plugin function is called once. The path is read once, regex matching is performed once, the new path is calculated, and it is written back. There is no "first calculate length then copy" two-pass design, so the possibility of two-pass state inconsistency naturally does not exist.

2. Runs inside WASM Sandbox

This is the most critical point. WASM plugins run in a sandboxed virtual machine:

  • Memory Isolation: The memory space of the plugin is completely isolated from the gateway process. Even if there is a buffer calculation error in the plugin code, the overflow only occurs in the linear memory of the WASM virtual machine and will not affect the heap of the host process (Envoy).
  • Restricted Capabilities: WASM plugins can only interact with the gateway through APIs provided by the Proxy-WASM SDK—reading/writing headers, reading/writing properties, and making HTTP requests. It cannot directly manipulate host process memory, call host functions, or access the file system.
  • Failure Isolation: If a plugin panics or crashes, the impact is limited only to the current request. It does not cause the entire worker process to crash, let alone lead to RCE.

In contrast to Nginx's C modules—any memory error occurs directly within the address space of the worker process, where a heap overflow can directly overwrite adjacent function pointers, making RCE the natural attack path.

3. Code Logic is Explicit

Nginx directives have implicit state propagation (the is_args flag is an example), which is neither documented nor easy to deduce from the configuration text.

In WASM plugins, all logic is explicit Go code. The assignment and passage of variables are clear at a glance, and there is no possibility of "the side effects of one directive quietly affecting the behavior of another." Code review and testing are much easier than auditing Nginx configurations.

Three levels of Security Architecture

Starting from this vulnerability, we can observe three levels of gateway security architecture:

Nginx Envoy Native Higress WASM
Configuration Method Imperative Declarative Code (Go/Rust)
State Management Implicit global state machine Stateless Explicit local variables
Memory Safety C language, unprotected C++, but core path is safe WASM sandbox isolation
Flexibility High (but with security hazards) Medium High (and safe)
Vulnerability Impact Worker process RCE Configuration analysis bug Only the current request fails

This is not to say Envoy or Higress is free of vulnerabilities. All software has bugs. But different architectural designs determine the blast radius of a vulnerability:

  • Nginx's imperative state leak → Host process heap overflow → RCE
  • Envoy's declarative configuration → Even if there is a bug, it remains at the configuration parsing level, without involving runtime state leakage
  • Higress WASM → Even if there is a bug in the plugin, it is trapped inside the sandbox, with the worst-case scenario being a 500 error for the current request

Conclusion

CVE-2026-42945 will not be the last "security vulnerability hidden within a configuration language." Any system trying to stuff Turing-complete capabilities into a configuration format will face the complexity of state management. Nginx's rewrite module was a reasonable engineering choice back in 2008; 18 years later today, we have better alternatives.

Envoy eliminated the attack surface of state leaks with declarative configuration, but sacrificed flexibility. Higress's WASM plugins, while retaining flexibility, fundamentally restrict the impact scope of vulnerabilities through sandbox isolation.

Replacing directives with code, replacing trust with sandboxes. This might just be the right direction for the evolution of gateway security.

If you are migrating from Nginx to Higress, the Higress community already has an nginx-rewrite-compatible WASM plugin that fully covers all features of rewrite + set, allowing you to directly replace vulnerable Nginx configurations.

References

0 1 0
Share on

You may also like

Comments

Related Products