Summary
This issue is broader than HTTP proxy request construction. The underlying bug class is that HTTP script / complex-value evaluation can recursively flush no-cacheable variables between a length pass and a write pass. Any code that sizes an output buffer using script length codes and later writes using script value codes can become unsafe if an intervening script evaluation invalidates a no-cacheable variable that was already counted.
nginx/nginx#1194 tracks the same root cause upstream. That issue was reported for FastCGI and notes SCGI/uWSGI applicability. This issue adds freenginx evidence for proxy headers/body and tracks the broader script-engine class.
Affected Area
Root cause:
src/http/ngx_http_script.c:606-650, ngx_http_script_run()
src/http/ngx_http_script.c:617-622, where all currently no-cacheable variables are invalidated
src/http/ngx_http_variables.c:1590-1591, $document_root with variable root
src/http/ngx_http_variables.c:1632-1633, $realpath_root with variable root
Vulnerable pattern:
- A module computes an output length from script length codes.
- A later script/complex-value evaluation calls
ngx_http_script_run() and flushes no-cacheable variables.
- The module writes using script value codes.
- A no-cacheable variable returns a different length during the write pass than it returned during the size pass.
Known request-construction sites using this pattern include:
src/http/modules/ngx_http_fastcgi_module.c, ngx_http_fastcgi_create_request()
src/http/modules/ngx_http_scgi_module.c, ngx_http_scgi_create_request()
src/http/modules/ngx_http_uwsgi_module.c, ngx_http_uwsgi_create_request()
src/http/modules/ngx_http_proxy_module.c, ngx_http_proxy_create_request()
src/http/modules/ngx_http_grpc_module.c, ngx_http_grpc_create_request()
Impact
A remote client can trigger this class when configuration combines:
- a no-cacheable or request-time variable whose value can change between evaluations, such as
$tcpinfo_* or a custom no-cacheable variable;
- a two-pass script serialization site, such as upstream request parameter/header/body construction;
- a later script evaluation that recursively flushes no-cacheable variables, such as
$document_root or $realpath_root with variable root.
Impact is at least worker crash / denial of service. Depending on the affected serializer, the mismatch can also corrupt generated upstream protocol data before the crash. Exploitability beyond crash was not assessed.
Expected Behavior
A script-generated buffer should be sized from the same variable values that are later written. Recursive script evaluation should not invalidate values that an outer length/write operation depends on.
Actual Behavior
ngx_http_script_run() invalidates all currently no-cacheable variables before running its own length and value scripts. If this happens inside another two-pass serialization operation, an earlier no-cacheable variable can be re-evaluated and return a larger value during the write pass than it returned during the length pass.
The result is an undersized output buffer or stale generated protocol length, followed by out-of-bounds writes or malformed upstream request data.
Evidence
The proxy request builder reproduces the broader class.
ASAN results from deterministic no-cacheable variable growth:
| Scenario |
Result |
| no later root flush |
ASAN clean; backend accepted request; one variable evaluation |
variable proxy_pass URI |
ASAN clean; request line used the first expanded URI |
proxy_set_header before $realpath_root |
ASAN heap-buffer-overflow; write during request-header serialization; allocation in ngx_http_proxy_create_request() |
proxy_set_header before $document_root |
ASAN heap-buffer-overflow; write during request-header serialization; allocation in ngx_http_proxy_create_request() |
proxy_set_body with later root flush |
ASAN heap-buffer-overflow during body serialization |
| non-ASAN default build with larger growth |
worker segfault before backend accept; variable grew from 5000 to 1053576 bytes |
All trigger logs showed the no-cacheable variable returning len=5000 during the sizing pass and a larger value during the write pass after $document_root / $realpath_root caused a recursive no-cacheable-variable flush.
Concrete proxy reproducer shape:
server {
listen 127.0.0.1:23541;
root $http_x_round18_root;
location / {
proxy_set_header X-Growing $round18_proxy_growing_var;
proxy_set_header X-Trigger $realpath_root;
proxy_pass http://127.0.0.1:23551;
}
}
The same trigger shape applies to other script-generated upstream request buffers: place the changing no-cacheable variable in a parameter/header/body value that participates in a length pass, then trigger a later $document_root / $realpath_root expansion from variable root before the write pass completes.
Suggested Fix
Fix the root script-engine behavior so recursive script evaluation cannot invalidate no-cacheable values that are already being used by an outer length/write operation.
Possible approaches:
- freeze/snapshot no-cacheable variables for the duration of a two-pass script operation;
- track script-evaluation nesting and avoid recursive invalidation of variables already consumed by an outer script operation;
- ensure each paired length/value pass uses the same variable values.
As a defense-in-depth measure, individual serializers should also verify that the write pass does not exceed the allocated buffer and that generated protocol lengths still match the bytes written. That guard should not replace a root fix, because stale protocol lengths can remain wrong even if extra allocation avoids an immediate memory overwrite.
Summary
This issue is broader than HTTP proxy request construction. The underlying bug class is that HTTP script / complex-value evaluation can recursively flush no-cacheable variables between a length pass and a write pass. Any code that sizes an output buffer using script length codes and later writes using script value codes can become unsafe if an intervening script evaluation invalidates a no-cacheable variable that was already counted.
nginx/nginx#1194tracks the same root cause upstream. That issue was reported for FastCGI and notes SCGI/uWSGI applicability. This issue adds freenginx evidence for proxy headers/body and tracks the broader script-engine class.Affected Area
Root cause:
src/http/ngx_http_script.c:606-650,ngx_http_script_run()src/http/ngx_http_script.c:617-622, where all currently no-cacheable variables are invalidatedsrc/http/ngx_http_variables.c:1590-1591,$document_rootwith variablerootsrc/http/ngx_http_variables.c:1632-1633,$realpath_rootwith variablerootVulnerable pattern:
ngx_http_script_run()and flushes no-cacheable variables.Known request-construction sites using this pattern include:
src/http/modules/ngx_http_fastcgi_module.c,ngx_http_fastcgi_create_request()src/http/modules/ngx_http_scgi_module.c,ngx_http_scgi_create_request()src/http/modules/ngx_http_uwsgi_module.c,ngx_http_uwsgi_create_request()src/http/modules/ngx_http_proxy_module.c,ngx_http_proxy_create_request()src/http/modules/ngx_http_grpc_module.c,ngx_http_grpc_create_request()Impact
A remote client can trigger this class when configuration combines:
$tcpinfo_*or a custom no-cacheable variable;$document_rootor$realpath_rootwith variableroot.Impact is at least worker crash / denial of service. Depending on the affected serializer, the mismatch can also corrupt generated upstream protocol data before the crash. Exploitability beyond crash was not assessed.
Expected Behavior
A script-generated buffer should be sized from the same variable values that are later written. Recursive script evaluation should not invalidate values that an outer length/write operation depends on.
Actual Behavior
ngx_http_script_run()invalidates all currently no-cacheable variables before running its own length and value scripts. If this happens inside another two-pass serialization operation, an earlier no-cacheable variable can be re-evaluated and return a larger value during the write pass than it returned during the length pass.The result is an undersized output buffer or stale generated protocol length, followed by out-of-bounds writes or malformed upstream request data.
Evidence
The proxy request builder reproduces the broader class.
ASAN results from deterministic no-cacheable variable growth:
proxy_passURIproxy_set_headerbefore$realpath_rootngx_http_proxy_create_request()proxy_set_headerbefore$document_rootngx_http_proxy_create_request()proxy_set_bodywith later root flush5000to1053576bytesAll trigger logs showed the no-cacheable variable returning
len=5000during the sizing pass and a larger value during the write pass after$document_root/$realpath_rootcaused a recursive no-cacheable-variable flush.Concrete proxy reproducer shape:
The same trigger shape applies to other script-generated upstream request buffers: place the changing no-cacheable variable in a parameter/header/body value that participates in a length pass, then trigger a later
$document_root/$realpath_rootexpansion from variablerootbefore the write pass completes.Suggested Fix
Fix the root script-engine behavior so recursive script evaluation cannot invalidate no-cacheable values that are already being used by an outer length/write operation.
Possible approaches:
As a defense-in-depth measure, individual serializers should also verify that the write pass does not exceed the allocated buffer and that generated protocol lengths still match the bytes written. That guard should not replace a root fix, because stale protocol lengths can remain wrong even if extra allocation avoids an immediate memory overwrite.