CVE-2026-42945(严重,CVSS 9.2)是 Nginx 于 2008 年就引入的一个严重堆缓冲区溢出漏洞。该漏洞使得使用rewriteset指令的服务器可被未经身份验证的远程攻击者执行任意代码。

漏洞触发条件

在受影响版本中,需满足以下条件:

  1. 使用了rewrite指令,并且该指令的替换字符串中包含问号?;
  2. 在同一个location或上下文中,在满足第 1 条rewrite指令之后,使用set指令来引用一个正则捕获组(如 $1$2

一个典型的危险配置示例如下:

location ~ ^/api/(.*)$ {
    # 1. rewrite 替换字符串中包含了 "?" (问号)
    rewrite ^/api/(.*)$ /internal?migrated=true;
    
    # 2. 之后使用 set 指令引用捕获组 $1
    set $original_endpoint $1;
}

影响范围

受影响版本:NGINX 开源版 0.6.27 至 1.30.0,以及多个相关的商业产品和模块(如 NGINX Plus、Ingress Controller 等)。

修复版本:官方已在 2026年5月13日发布的安全公告中提供了修复(稳定版 1.30.1、主线版 1.31.0)。如果你使用的版本在上述范围内,应尽快升级。

CVE-2026-42945 详情

此漏洞需要使用rewriteset指令才能触发;

rewrite指令基于正则表达式修改请求的 URI。当请求匹配指定的模式时,Nginx 会用新的字符串替换原 URI。

例如:

rewrite ^/api/(.*)$ /v2/api/$1
# /api/get   =>  /v2/api/get

括号中匹配到的内容通过$1变量追加到新路径后。

正则表达式括号内的部分称为捕获组,可以存在多个;

在 Nginx 配置文件中,$1 ~ $9 匹配上下文中最近执行的正则表达式的第 1 ~ 9 个捕获组;

如果替换字符串中包含问号,Nginx 会将问号后的部分视为查询字符串,并将原始的请求参数附加到其后。

set指令用于为自定义变量赋值。这在实践中非常有用,例如临时存储原始请求的部分内容、动态路由端点,或在后续重写操作更改 URI 之前,在请求生命周期内维护状态。

它像rewrite指令一样可以使用$1引用最近执行的正则表达式中的捕获组。例如:

set $original_path $1
# original_path = "get"

将匹配到的内容保存到自定义变量original_path中。这样可以确保即使 URI 被完全重写,后端应用程序或访问日志仍然可以访问原始请求的路径。

触发漏洞

在底层,Nginx 通过其脚本引擎优化这些操作。解析配置时,脚本引擎会将这些指令编译成一系列操作序列。在运行时,引擎会分两遍执行这些操作:第一遍计算最终字符串的总长度,以便从其内存池中精确分配所需大小的内存;第二遍执行复制操作,将实际数据写入新分配的缓冲区。这种设计避免了多次小内存分配,但要求第一遍计算出的长度必须与第二遍实际写入的数据量完全一致。如果引擎状态在这两遍之间发生变化,就可能引发内存破坏漏洞。

具体来说,当替换的字符串中包含问号时:

rewrite ^/api/(.*)$ /internal?migrated=true;

会触发一个叫ngx_http_script_start_args_code的函数:

本文使用的 Nginx 代码都位于src/http/ngx_http_script.c文件中;

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);
}

该函数会为脚本引擎设置一个标志,该标志在脚本代码执行期间永远不会重置。

e->is_args = 1;

当后续set指令引用正则表达式捕获组时,会触发另外一个ngx_http_script_complex_value_code函数:

    // ngx_http_script_complex_value_code 函数片段
    ...

    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;

    ...

此函数会使用一个全新的、完全清零的子引擎le,因为它初始化为 0,所以le.is_args值为 0;

长度计算函数ngx_http_script_copy_capture_len_code会检查以下条件来决定是否需要转义:

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];
    }
    
    ...
}

因为le.is_args为 0,程序会跳转到else分支,直接返回原始的、未转义的长度。

然而,在第二次复制过程中,复制函数ngx_http_script_copy_capture_code在主引擎上运行,此时e.is_args仍然设置为 1,导致进入了另一个逻辑分支:

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]);
    }

    ...
}

它会调用一个使用 NGX_ESCAPE_ARGS 标志的函数ngx_escape_uri。该函数会将每个可转义字符从一个字节扩展到三个字节。

由于状态发生了意外变化,复制函数会将raw_size + 2 * N字节的数据写入一个仅分配了raw_size字节的缓冲区中(其中 N 为可转义字符的数量),这种不匹配导致数据完全溢出所分配的内存池边界。

概念验证

  • https://github.com/DepthFirstDisclosures/Nginx-Rift