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:
- Capture source: a regex
location ~,server_name ~,rewrite,if (... ~ ...), or a regexmapwith a capture group. - Clobber trigger: a
map(http or stream) with a~or~*regex match key, referenced by its output variable. - 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.
- single-value directives (
- 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
- 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.
- Interim config hardening (defence in depth, not a substitute for the patch):
- Do not reference a regex
mapvariable in the same location or buffer as a$1to$9or 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_headerorfastcgi_paramfamily.
- Do not reference a regex
Limitations
- Static analysis: it reads config, not runtime behaviour. It cannot resolve
values produced by dynamically
setvariables. - Unreadable
includetargets (for example absolute paths that do not exist locally) are reported undermissing_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/
Comments