A read-only static scanner that flags nginx configurations vulnerable to the complex_value two-pass capture-clobbering bug class (F5/nginx SIRT advisory, July 2026). It parses your config and reports the vulnerable pattern with the reason and location. It does not exploit anything and does not touch the running server.

Single file, standard library only, Python 3.6+.

Full write-up of the vulnerability: https://cyberstan.co.uk/nginx-rce/

The bug

nginx builds many directive values in two passes. A LEN pass measures the length and allocates a buffer, then a VALUE pass writes into it. Regex captures ($1 to $9, or named (?P<name>...)) are read from per-request shared state. A map whose match key is a regular expression runs a fresh PCRE match the moment its variable is evaluated, and that match overwrites the shared capture state. If a capture is measured in the LEN pass and then a regex map runs before the VALUE pass rewrites that capture, the two passes disagree:

  • clobber makes the value longer than measured: heap buffer overflow (attacker-controlled out-of-bounds write)
  • clobber makes the value shorter than measured: uninitialised heap returned to the client (info leak of libc/heap pointers)

Both directions are remotely triggerable when the map input is attacker-influenced ($request_body, $http_*, $arg_*, $uri, $ssl_preread_server_name, and so on).

What counts as vulnerable

The scanner flags a location or server when all of these land in one two-pass buffer, in the exploitable order:

  1. Capture source: a regex location ~, server_name ~, rewrite, if (... ~ ...), or a regex map with a capture group.
  2. Clobber trigger: a map (http or stream) with a ~ or ~* regex match key, referenced by its output variable.
  3. Two-pass sink using both a capture ref and the regex-map variable:
    • single-value directives (return, set, add_header, proxy_method, proxy_pass, rewrite, index, and others): both refs in the same value.
    • the param/header families (proxy_set_header, proxy_set_body, fastcgi_param, scgi_param, uwsgi_param, grpc_set_header): these build one buffer per request for the whole family, so the two refs can be in separate directives in the same location.
  4. Order: the capture must be evaluated before the regex map. A regex map runs once and caches, so map-before-capture reads a consistent state and is not vulnerable. The scanner reports those as cleared (safe but fragile, do not reorder).

Usage

# scan a single config (follows include directives by default)
python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf

# scan a directory (uses nginx.conf if present, else all *.conf)
python3 nginx_capture_clobber_scan.py /etc/nginx/

# scan several roots
python3 nginx_capture_clobber_scan.py site-a.conf site-b.conf

# machine-readable output
python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf --json

# do not expand include directives
python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf --no-includes

Options

Option Effect
--json Emit a JSON report instead of text.
--no-includes Do not follow include directives.

Exit codes

Code Meaning
0 No vulnerable configuration found.
1 At least one vulnerable configuration found.
2 Usage error (no path given, or none found).

Usable in CI, since a vulnerable config exits non-zero:

python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf || echo "review needed"

Output

Text mode prints, for each finding: file and line, the enclosing scope, the sink directive, the capture and map variables involved, the triggering map, and a one-line reason. A cleared section lists safe-order coexistences. --json produces:

{
  "vulnerable": true,
  "findings": [
    {
      "file": "/etc/nginx/conf.d/api.conf",
      "line": 12,
      "scope": "location ~ ^/api/(.+)$",
      "directive": "proxy (family buffer)",
      "captures": ["$1"],
      "map_vars": ["$is_bot"],
      "triggers": ["$is_bot <- regex-map on $http_user_agent [nginx.conf:30]"],
      "reason": "..."
    }
  ],
  "cleared_safe_order": [],
  "regex_maps": [],
  "missing_includes": []
}

Fixing what it finds

  1. Apply the vendor patch (nginx 1.30.4 stable / 1.31.3 mainline, NGINX Plus R36 P7 / 37.0.3.1). This is the only complete fix. The patch adds a boundary guard (ngx_http_script_check_length()) that prevents the VALUE pass from writing past the allocated buffer, and computes the returned string length from bytes actually written rather than the LEN-pass measurement, closing both the overflow and the info-leak directions.
  2. Interim config hardening (defence in depth, not a substitute for the patch):
    • Do not reference a regex map variable in the same location or buffer as a $1 to $9 or named-capture reference.
    • Do not reuse the same (?P<name>...) group across multiple regex maps.
    • Keep capture refs and regex-map refs out of the same proxy_set_header or fastcgi_param family.

Limitations

  • Static analysis: it reads config, not runtime behaviour. It cannot resolve values produced by dynamically set variables.
  • Unreadable include targets (for example absolute paths that do not exist locally) are reported under missing_includes, not silently skipped. Run it on the host where the config resolves for full coverage.
  • A flagged capture-before-map hit can be defused at runtime if that map variable was already evaluated earlier in the same request (it would already be cached). The scanner still reports these, which is the conservative choice for a security self-scan.

Reference

Vulnerability write-up and exploit chain: https://cyberstan.co.uk/nginx-rce/