CVE-2026-42945 (Critical, CVSS 9.2) is a severe heap buffer overflow vulnerability introduced in Nginx in 2008. This vulnerability allows unauthenticated remote attackers to execute arbitrary code on servers using the rewrite and set directives.
Trigger Conditions
In affected versions, the following conditions must be met:
- The
rewritedirective is used, and its replacement string contains a question mark?; - In the same
locationor context, after therewritedirective satisfying condition 1, thesetdirective is used to reference a regex capture group (e.g.,$1,$2).
A typical dangerous configuration example is as follows:
location ~ ^/api/(.*)$ {
# 1. rewrite replacement string contains "?" (question mark)
rewrite ^/api/(.*)$ /internal?migrated=true;
# 2. Then use set directive to reference capture group $1
set $original_endpoint $1;
}
Affected Versions
Affected versions: NGINX Open Source 0.6.27 to 1.30.0, and multiple related commercial products and modules (e.g., NGINX Plus, Ingress Controller, etc.).
Fixed versions: The official security advisory released on May 13, 2026 provides fixes (stable version 1.30.1, mainline version 1.31.0). If you are using a version within the affected range, upgrade as soon as possible.
CVE-2026-42945 Details
This vulnerability requires the use of rewrite and set directives to trigger;
The rewrite directive modifies the request URI based on a regular expression. When a request matches the specified pattern, Nginx replaces the original URI with the new string.
For example:
rewrite ^/api/(.*)$ /v2/api/$1
# /api/get => /v2/api/get
The content matched in parentheses is appended to the new path via the $1 variable.
The part inside the regex parentheses is called a capture group; there can be multiple;
In Nginx configuration files, $1 ~ $9 match the 1st to 9th capture groups of the most recently executed regex in the context;
If the replacement string contains a question mark, Nginx treats the part after the question mark as a query string and appends the original request parameters to it.
The set directive is used to assign values to custom variables. This is very useful in practice, for example, to temporarily store parts of the original request, dynamically route endpoints, or maintain state during the request lifecycle before subsequent rewrite operations change the URI.
Like the rewrite directive, it can use $1 to reference capture groups from the most recently executed regex. For example:
set $original_path $1
# original_path = "get"
This saves the matched content into the custom variable original_path. This ensures that even if the URI is completely rewritten, the backend application or access log can still access the original request path.
Triggering the Vulnerability
Under the hood, Nginx optimizes these operations through its script engine. When parsing the configuration, the script engine compiles these directives into a sequence of operations. At runtime, the engine executes these operations in two passes: the first pass calculates the total length of the final string to allocate exactly the required memory from its memory pool; the second pass performs the copy operation, writing the actual data into the newly allocated buffer. This design avoids multiple small memory allocations but requires that the length calculated in the first pass exactly matches the amount of data written in the second pass. If the engine state changes between these two passes, memory corruption vulnerabilities can occur.
Specifically, when the replacement string contains a question mark:
rewrite ^/api/(.*)$ /internal?migrated=true;
This triggers a function called ngx_http_script_start_args_code:
The Nginx code used in this article is located in the src/http/ngx_http_script.c file;
void
ngx_http_script_start_args_code(ngx_http_script_engine_t *e)
{
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0,
"http script args");
e->is_args = 1;
e->args = e->pos;
e->ip += sizeof(uintptr_t);
}
This function sets a flag in the script engine that is never reset during script code execution.
e->is_args = 1;
When the subsequent set directive references a regex capture group, it triggers another function ngx_http_script_complex_value_code:
// ngx_http_script_complex_value_code function snippet
...
ngx_http_script_engine_t le;
...
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
le.ip = code->lengths->elts;
le.line = e->line;
le.request = e->request;
le.quote = e->quote;
...
This function uses a brand new, fully zeroed sub-engine le. Since it is initialized to 0, le.is_args is 0;
The length calculation function ngx_http_script_copy_capture_len_code checks the following condition to decide whether escaping is needed:
size_t
ngx_http_script_copy_capture_len_code(ngx_http_script_engine_t *e)
{
...
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
p = r->captures_data;
return cap[n + 1] - cap[n]
+ 2 * ngx_escape_uri(NULL, &p[cap[n]], cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
} else {
return cap[n + 1] - cap[n];
}
...
}
Because le.is_args is 0, the program jumps to the else branch, directly returning the original, unescaped length.
However, during the second copy pass, the copy function ngx_http_script_copy_capture_code runs on the main engine, where e.is_args is still set to 1, causing it to enter another logic branch:
void
ngx_http_script_copy_capture_code(ngx_http_script_engine_t *e)
{
...
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
e->pos = (u_char *) ngx_escape_uri(pos, &p[cap[n]],
cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
} else {
e->pos = ngx_copy(pos, &p[cap[n]], cap[n + 1] - cap[n]);
}
...
}
It calls the function ngx_escape_uri with the NGX_ESCAPE_ARGS flag. This function expands each escapable character from one byte to three bytes.
Due to the unexpected state change, the copy function writes raw_size + 2 * N bytes of data into a buffer that was allocated only raw_size bytes (where N is the number of escapable characters). This mismatch causes data to completely overflow the allocated memory pool boundary.
Proof of Concept
- https://github.com/DepthFirstDisclosures/Nginx-Rift