CVE-2026-42945实战:nginx rewrite堆溢出漏洞分析、检测与修复全指南

2026年5月13日,NGINX官方推送安全补丁,修复了一个编号为CVE-2026-42945的堆缓冲区溢出漏洞,社区给它起了个代号叫NGINX Rift。

这个漏洞从2008年的0.6.27版本就埋进了代码里,整整潜伏了18年。触发点小到离谱——rewrite规则里的一个问号,搭配链式的指令写法,就能捅穿堆内存。

漏洞公开当天,互联网上就出现了批量扫描流量。攻击者只需要发一条普通HTTP请求,就能打崩NGINX的worker进程;如果目标服务器关闭了ASLR,甚至可以稳定执行任意代码,直接接管服务器。

国内很多云厂商、CDN服务商第一时间做了紧急修复,但大量中小企业和个人站长的站点还处于裸奔状态。很多运维甚至不知道自己的配置踩了坑,毕竟带问号的rewrite加set变量,是太常见的写法。

本文从源码层面拆解漏洞的完整逻辑,复现触发条件,给出可直接落地的检测脚本、修复方案和配置规范,你看完可以直接上手排查线上环境。


一、漏洞基础盘:你需要先知道的核心事实

先把最关键的信息列出来,不用绕弯。

  • 漏洞编号:CVE-2026-42945,代号NGINX Rift
  • 漏洞类型:堆缓冲区溢出(Heap Buffer Overflow)
  • CVSS 4.0评分:9.2,严重等级
  • 引入版本:NGINX 0.6.27(2008年10月发布)
  • 受影响范围:开源版 0.6.27 到 1.30.0 全版本;NGINX Plus R36及以下所有商业版本
  • 修复版本:开源版 1.30.1(稳定版)、1.31.0(主线版);Plus版 R36 P4、R32 P6
  • 攻击门槛:远程未授权,单条HTTP请求即可触发
  • 危害上限:默认配置下稳定触发拒绝服务;ASLR关闭环境下可实现远程代码执行,获取服务器控制权

这个漏洞影响的不只是原生NGINX。所有基于NGINX二次开发的网关、反向代理、Ingress控制器,只要保留了rewrite模块的原生逻辑,都受影响。比如K8s常用的ingress-nginx,默认配置就大量使用rewrite和set指令,很多集群直接踩中了触发条件。 根据公开的测绘数据,全球公网暴露的NGINX服务器超过1.3亿台,其中约40%的配置存在链式rewrite写法,实际受威胁的机器数量远超大部分同类漏洞。


二、从源码拆解:两阶段引擎的状态撕裂是怎么发生的

要理解这个漏洞,得先搞懂NGINX的rewrite模块为什么要设计成两阶段处理,以及is_args标志位到底管什么用。

2.1 两阶段设计的初衷

NGINX的内存管理采用内存池机制,尽量避免频繁的小块内存申请释放,减少内存碎片,提升性能。

处理rewrite替换的时候,最终生成的URI长度不固定。如果边写边扩容缓冲区,会涉及多次内存分配和拷贝,性能损耗很大。所以NGINX设计了两遍扫描的处理逻辑:

第一遍叫长度计算阶段,只遍历替换逻辑,统计最终字符串的总字节数,不实际写数据。

第二遍叫数据写入阶段,根据第一遍算出的长度,从内存池一次性申请刚好大小的缓冲区,再把内容完整写进去。

这个设计本身没问题,是典型的用计算换性能的思路。问题出在两遍扫描用的引擎状态不一致。

2.2 is_args标志位的作用

NGINX的脚本引擎里有个叫is_args的标志位,用来标记当前字符串是否已经进入查询参数部分,也就是问号?之后的内容。

当引擎解析到问号时,会把is_args置为1。后续往缓冲区写内容的时候,如果is_args为1,就会自动对写入的内容做URL参数转义,把+、&、=、%、#这类特殊字符转换成%XX的格式,避免破坏URI结构。

1个原始字符转义后变成3个字符,长度直接膨胀2倍。这个膨胀量,正常情况下应该在长度计算阶段就预留出来。

2.3 长度计算阶段:清零的子引擎

长度计算阶段不会直接用主引擎执行,而是会创建一个全新的临时子引擎le,整个结构体清零初始化,所有标志位都是默认值0。

// 简化自 src/http/ngx_http_script.cngx_http_script_engine_tle;ngx_memzero(&le,sizeof(ngx_http_script_engine_t));le.sp=e->sp;le.buf=e->buf;

这个子引擎只负责累加长度,不处理实际业务逻辑。执行到问号对应的操作码时,理论上也应该把le.is_args置1,这样后续计算捕获组长度的时候,才能预留转义膨胀的空间。

但问题出在匿名捕获组的长度计算函数上。ngx_http_script_copy_capture_len这个函数,根本没有读取is_args标志位的逻辑,不管标志位是0还是1,它都直接返回捕获组的原始字节长度。

// 匿名捕获组长度计算函数(缺陷核心)staticsize_tngx_http_script_copy_capture_len(ngx_http_script_engine_t*e,ngx_uint_tn){if(e->captures==NULL||n>=e->ncaptures){return0;}// 直接返回原始长度,完全不考虑is_args和转义膨胀returne->captures[2*n+1]-e->captures[2*n];}

也就是说,哪怕长度计算阶段的le.is_args被正确置1了,匿名捕获组的长度还是按原始值算,根本没留转义的余量。这是漏洞的第一层缺陷。

2.4 数据写入阶段:带状态的主引擎

到了真正写数据的阶段,用的是主引擎e,所有标志位都会保留之前的状态。

主引擎执行到问号的时候,e->is_args会被置1,并且这个标志一旦设置,后续所有写入操作都会受影响。当执行到匿名捕获组的拷贝逻辑时,函数会先判断is_args状态,再决定要不要转义。

// 匿名捕获组数据写入函数staticvoidngx_http_script_copy_capture(ngx_http_script_engine_t*e,ngx_http_script_copy_capture_code_t*code){size_tlen;u_char*p;ngx_int_tn;n=code->n;if(e->captures==NULL||(size_t)n>=e->ncaptures){return;}p=e->ip;len=e->captures[2*n+1]-e->captures[2*n];// 关键判断:is_args为1就执行转义,长度膨胀if(e->is_args){e->ip=ngx_escape_uri(e->ip,e->captures[2*n],len,NGX_ESCAPE_ARGS);return;}e->ip=ngx_copy(p,e->captures[2*n],len);}

长度按1字节算,写入按3字节写,缓冲区必然越界。溢出的字节会直接覆盖堆上相邻的内存区域,这就是堆溢出的直接成因。

2.5 最隐蔽的坑:标志位跨指令泄漏

如果只是单条rewrite内部的问题,影响范围还没那么大。真正让漏洞覆盖面暴涨的,是is_args标志位会跨指令泄漏。

NGINX的rewrite模块会把同一个location块下的所有rewrite、set、if指令编译成一段连续的字节码,由同一个主引擎顺序执行。前面的指令设置了e->is_args = 1之后,后面的指令不会主动重置这个标志位,会一直带着这个状态执行。

举个最常见的配置例子:

location /api/ { # 第一条指令:替换串带问号,执行后e->is_args被置1 rewrite ^/api/(.*)$ /internal_api?version=v2 last; # 第二条指令:使用匿名捕获组$1,此时主引擎is_args已经是1 set $real_path $1; }

执行第二条set指令的时候,长度计算阶段会新建清零的子引擎le,le.is_args = 0,按原始长度计算缓冲区大小。

但真正写入的时候,用的是主引擎e,e->is_args已经被第一条rewrite置成了1,所以会强制对$1的内容做URL转义。

两条指令加起来,刚好凑齐了“长度算少了,写入写多了”的全部条件。这也是为什么大部分踩坑的配置都是链式写法——单条rewrite自己的内部逻辑还能保持一致,跨指令之后状态直接脱节。

主引擎e生命周期

指令1:rewrite带?

设置e.is_args=1

指令2:set $var $1

长度计算:新建le,le.is_args=0

按原始长度分配缓冲区

数据写入:复用e,e.is_args=1

执行URL转义,内容膨胀

堆缓冲区溢出

很多运维写配置的时候,根本不会意识到前后两条指令会有状态关联。毕竟从配置语法上看,两条指令是独立的,谁能想到一个问号的影响会顺着执行流往下传。

三、触发条件精准判定:三个核心前提缺一不可

不是所有带问号的rewrite都会触发漏洞,必须同时满足三个核心条件,少一个都不行。

条件1:存在匿名正则捕获组

只有$1、$2、$3这类按序号引用的匿名捕获组,才会走前面说的有缺陷的代码路径。

如果用的是命名捕获组,比如(?.*)对应$path,走的是另一套变量处理逻辑,不存在长度计算不一致的问题,完全不会触发漏洞。

另外,$uri、$arg_id这类内置变量,也不会触发这个漏洞。

条件2:主引擎is_args被置1

也就是配置链里必须出现过带问号?的替换字符串。问号可以在任何位置,哪怕后面没有实际参数,只是个空问号,也会把is_args置1。 这个状态一旦设置,后面所有同一块内的指令都会继承这个状态,直到请求处理结束。

条件3:捕获内容包含可转义字符

捕获组匹配到的内容里,必须包含+、&、=、%、#、?这类需要URL参数转义的特殊字符。 如果捕获的内容全是普通字母数字,转义前后长度一样,缓冲区刚好够写,不会触发溢出。攻击者只要构造一串连续的特殊字符,比如几十个+,就能轻松触发溢出。

三个条件凑齐,漏洞就会被触发。攻击者不需要任何权限,只要构造对应的URI发请求就行。比如上面的例子里,只要请求/api/++++++++++++++++++++++++++++++,就能直接打崩worker进程。

这里纠正一个常见误区:很多人以为只有rewrite指令会触发,其实set指令和if指令只要用到匿名捕获组,并且前面的指令置了is_args,一样会触发。比如用if做正则匹配,然后后面用$1赋值,前面有带问号的rewrite,同样会出问题。

四、攻击利用链路:从进程崩溃到远程接管

堆溢出的危害上限很高,但实际利用难度和目标环境的防护强度直接相关。我们从易到难拆解完整的利用路径。

4.1 最低门槛:单请求DoS拒绝服务

这是最容易实现,也是当前野外攻击最常用的方式。

攻击者构造一个包含大量特殊字符的URI,让它匹配到目标的rewrite规则。转义之后的内容长度远超缓冲区大小,溢出的数据会破坏堆上的内存结构,导致worker进程触发段错误,直接崩溃退出。

NGINX的master进程会自动拉起新的worker进程,但攻击者只要持续发请求,就能让worker反复崩溃,站点持续返回502,完全无法提供服务。

这种攻击成本极低,不需要任何复杂的利用技巧,一条curl命令就能打瘫一个站点。

# 示例请求(仅用于自测验证,禁止用于非法攻击)curl"http://target.com/api/$(python3-c'print("+"*1000)')"

正常配置的NGINX收到这种请求,error.log里会立刻出现worker process exited on signal 11 (SIGSEGV)的日志,对应进程崩溃。

4.2 进阶利用:堆内存信息泄露

如果控制好溢出的长度和内容,攻击者可以做到只溢出一点点,篡改相邻内存块的长度字段,让后续的读取操作返回更多数据。

NGINX的内存池里会存放请求头、响应体、会话密钥、用户cookie这类敏感数据。通过构造特定的溢出载荷,攻击者可以让NGINX在返回响应的时候,带上堆里的其他内存数据,从而泄露敏感信息。

比如泄露其他用户的会话令牌、后端服务的认证密钥,甚至NGINX配置里的密码。这类信息泄露往往会成为进一步入侵的跳板。

4.3 高阶利用:关闭ASLR环境下的稳定RCE

如果目标服务器关闭了地址空间布局随机化(ASLR),攻击者可以实现稳定的远程代码执行,直接拿到服务器权限。

发送多轮构造请求

堆喷布局可控内存块

触发堆溢出,覆写内存池next指针

劫持内存分配目标地址

后续内存分配请求

返回攻击者可控的内存地址

执行预设的Shellcode

获取服务器控制权,执行任意命令

具体利用逻辑分为几步:

第一步堆喷。攻击者连续发送大量构造好的请求,在NGINX的内存池里布局大量包含Shellcode的内存块,让堆内存分布变得可控。

第二步篡改指针。触发堆溢出,覆写相邻内存池块的next链表指针,把它改成攻击者想要的目标地址。

第三步劫持分配。当NGINX下次申请内存的时候,会顺着被篡改的链表走,把分配地址指向攻击者布局好的内存区域。

第四步执行代码。后续的写入操作会把数据写到可控地址,最终劫持程序执行流,运行Shellcode。

NGINX的worker进程通常以nginx用户运行,拿到权限之后攻击者可以进一步提权,最终接管整台服务器。

目前公网已经有成熟的RCE利用脚本流出,针对关闭ASLR的老系统,成功率非常高。

五、为什么藏了18年才被发现

一个存在了18年的漏洞,覆盖几乎所有主流版本,为什么直到2026年才被挖出来?这里面有几个很现实的原因。

首先是触发条件足够隐蔽。

要触发这个漏洞,得同时满足链式指令、问号、匿名捕获组、特殊字符四个条件。常规的功能测试只会验证rewrite能不能正常跳转,不会专门构造满是特殊字符的URI去测边界。大部分安全审计也只会盯着单条指令看,很难想到跨指令的状态泄漏问题。

其次是代码逻辑的割裂感很强。

长度计算和数据写入分属两个不同的函数,分散在源码的不同位置。标志位的设置和使用又跨了多条指令的执行流。人工审计的时候,很难把这几部分逻辑串起来,自然很难发现状态不一致的问题。 这次发现漏洞的Depthfirst团队,就是用AI大模型对NGINX全量代码做数据流分析,才顺着状态传递的路径揪出了这个问题。换成纯人工审计,可能再放几年也不一定能发现。

第三是业务场景的演进放大了暴露面。

2008年NGINX刚出来的时候,大多用来做静态文件服务器,rewrite用的不多,链式写法更是少见。最近十年随着微服务、API网关、K8s Ingress的普及,带参数的rewrite和变量赋值成了标配配置,几乎每个网关都会写好几段链式规则。 用的人多了,暴露面大了,漏洞被挖到的概率自然就高了。换在十年前,就算有这个漏洞,也没多少场景能触发。

最后是测试用例的盲区。

NGINX官方的测试集里,没有覆盖“多指令链+问号+匿名捕获组+特殊字符”的边界场景。这么多年迭代下来,没人碰过这个角落,代码一直没改,漏洞就一直留着。

六、实战自查:一键检测脚本与排查方法

不用等官方通知,你现在就可以自查自己的环境有没有风险。我整理了一套可直接复制的检测脚本,覆盖版本检查、配置检测、日志排查三个维度。

6.1 版本快速检测

先看自己的NGINX版本是不是在受影响范围内,一条命令搞定:

nginx-v2>&1|awk-F/'{print $2}'

输出版本号在0.6.27到1.30.0之间的,都存在基础漏洞风险。但最终有没有危险,还要看配置有没有踩中触发条件。

6.2 危险配置一键检测脚本

这个脚本会遍历你指定的NGINX配置目录,自动识别符合触发条件的危险配置,输出风险位置和对应的规则。

#!/bin/bash# NGINX CVE-2026-42945 危险配置检测脚本# 用法:bash nginx_rift_check.sh /etc/nginxCONFIG_DIR=${1:-/etc/nginx}RISK_COUNT=0echo"=== CVE-2026-42945 危险配置检测 ==="echo"扫描配置目录:$CONFIG_DIR"echo"-----------------------------------"# 遍历所有.conf文件find"$CONFIG_DIR"-typef-name"*.conf"|whileread-rfile;do# 初筛:匹配带问号且包含$数字捕获组的rewrite/set指令# 注:跨指令链式风险需结合上下文人工确认risk_lines=$(grep-nE"rewrite.*\?|\$[0-9]""$file"2>/dev/null)ifecho"$risk_lines"|grep-q"rewrite.*\?"&&echo"$risk_lines"|grep-q'\$[0-9]';thenecho"[!] 发现潜在风险文件:$file"echo"$risk_lines"|head-15echo"-----------------------------------"RISK_COUNT=$((RISK_COUNT+1))fidoneecho""echo"检测完成。"echo"提示:本脚本为初筛工具,跨文件include、嵌套block的链式风险需人工复核。"

把脚本保存为nginx_rift_check.sh,加执行权限后运行,指定你的NGINX配置目录即可。 脚本做的是初筛,能命中大部分明显的风险配置。对于跨多行、跨文件include的链式指令,还需要人工核对。最稳妥的方式是直接升级版本,一劳永逸。

6.3 异常日志排查

如果漏洞已经被触发过,NGINX的错误日志里会留下明确的痕迹。执行这条命令,排查最近7天的崩溃记录:

grep-i"signal 11\|SIGSEGV\|worker process exited"/var/log/nginx/error.log|tail-20

如果频繁出现worker进程异常退出的记录,并且时间和扫描流量吻合,很可能已经有人在尝试攻击你的站点。

6.4 无害验证方法

如果你想确认自己的配置会不会真的触发溢出,可以用少量特殊字符做测试,不要直接用大长度载荷打崩服务。建议先在测试环境验证。

# 低风险测试,建议测试环境执行curl-v"http://your-domain.com/path/++++++++++++++"

如果请求直接返回502,并且对应worker进程重启,说明配置确实触发了漏洞,需要立刻处理。

七、修复与加固:从临时缓解到永久修复

针对不同的场景,有不同的处理方案。能升级的优先升级,不能升级的用配置缓解,再配合WAF做防护,构成纵深防御。 注意:配置缓解只能封堵CVE-2026-42945这一个漏洞的触发路径,本次官方补丁还修复了另外三个同模块的安全问题,有条件的还是优先升级版本。

7.1 永久修复:升级到安全版本

升级NGINX到官方修复版本,是最彻底的解决方案,从源码层面修正了长度计算逻辑,一劳永逸。

产品类型修复版本
NGINX 开源稳定版1.30.1 及以上
NGINX 开源主线版1.31.0 及以上
NGINX Plus R36R36 P4 补丁包
NGINX Plus R32R32 P6 补丁包

官方的修复思路很直接:在长度计算阶段,针对匿名捕获组也加入is_args判断,按照转义后的最大长度预留缓冲区空间,保证分配的内存一定能装下转义后的内容。两段逻辑对齐了,溢出自然就不存在了。

7.2 临时缓解:配置层面规避

如果业务环境不能随便升级,比如老系统依赖特定版本的NGINX,可以用配置修改的方式规避触发条件,效果同样可靠。

方法一:匿名捕获组改命名捕获组

这是官方推荐的临时缓解方案,改造成本最低,效果最彻底。命名捕获组走独立的变量处理代码路径,完全不会触发漏洞。

# 危险配置 rewrite ^/api/(.*)$ /internal?version=v2 last; set $real_path $1; # 安全配置:改用命名捕获组 rewrite ^/api/(?.*)$ /internal?version=v2 last; set $real_path $real_path;

改完之后路由逻辑完全不变,只是把按序号引用改成了按名字引用,直接切断了漏洞触发路径。

方法二:拆分指令链,隔离状态

如果不想改捕获组写法,也可以把带问号的指令和带捕获组的指令分到不同的处理阶段,避免is_args标志跨指令传递。 比如用内部跳转拆分两个处理步骤:

location /api/ { rewrite ^/api/(.*)$ /internal_api/$1 last; } location /internal_api/ { rewrite ^(.*)$ $1?version=v2 break; proxy_pass http://backend; }

两个location各自独立处理,is_args状态不会互相传递,自然不会触发漏洞。

方法三:WAF规则拦截

如果配置也改不了,可以在WAF层加拦截规则,挡住恶意攻击载荷。核心是拦截URI中包含大量连续特殊字符的请求。 这里给一份ModSecurity可用的拦截规则:

# CVE-2026-42945 防护规则 SecRule REQUEST_URI "@rx (\+|&|=|%2B|%26|%3D){50,}" \ "id:900101,\ phase:1,\ block,\ status:403,\ msg:'NGINX Rift CVE-2026-42945 攻击尝试',\ severity:CRITICAL"

规则会拦截URI中包含50个以上连续特殊字符的请求,基本能挡住所有自动化扫描攻击。缺点是只能防已知攻击模式,遇到变形载荷可能绕过,只能作为临时补充手段。

7.3 长期加固:安全配置基线

除了修复这个漏洞,你还可以顺便优化NGINX的安全配置基线,降低同类漏洞的风险。

  • 所有rewrite规则优先使用命名捕获组,不用匿名序号捕获。
  • 尽量减少链式set和rewrite指令,复杂逻辑拆分到不同location。
  • 系统层面强制开启ASLR,不要为了调试方便关闭内存随机化。
  • worker进程运行在低权限用户下,不要用root启动worker。
  • 定期升级NGINX版本,跟进官方安全补丁。

八、延伸思考:基础软件的技术债与安全困局

这个漏洞爆出来之后,圈内讨论最多的不是漏洞本身有多厉害,而是“这么简单的问题居然藏了18年”。

其实仔细想就不奇怪。基础软件的代码迭代周期很长,很多十几年前写的逻辑,后面的开发者只会在上面加功能,很少敢动底层核心逻辑。一来怕改出兼容性问题,二来老代码的逻辑盘根错节,没人敢说自己完全摸透了所有分支。

NGINX的两阶段设计,在当年是非常优秀的性能优化思路。当年写代码的时候,可能根本没考虑到链式指令+状态传递的组合场景。后面的人加功能的时候,也只是顺着原有的框架写,没人回头去修正长度计算的逻辑。技术债就这么一点点攒下来,直到某一天业务场景变了,刚好踩中了那个死角,漏洞就爆了。

这类基础组件的漏洞,往往潜伏期长,影响面广,一旦爆发就是大面积的安全事件。过去几年OpenSSL的Heartbleed、Log4j的Log4Shell、Polkit的pkexec漏洞,都是同一个路数——藏了十几年的老代码,突然变成了全网级别的安全灾难。

未来这种情况只会越来越多。一方面软件的复杂度在指数级上升,人工审计根本覆盖不过来;另一方面云原生普及之后,基础组件的暴露面越来越大,攻击价值越来越高,黑客和安全研究者都会盯着这块挖。

好在现在AI辅助代码审计开始成熟。这次NGINX Rift漏洞就是AI辅助发现的,机器可以顺着数据流把分散在不同函数、不同模块的逻辑串起来,找到人工看不到的状态泄漏点。未来几年,AI审计会成为基础软件安全的标配,大量沉睡的老旧漏洞会被陆续挖出来。

对运维和安全从业者来说,别抱着“老软件稳定安全”的错觉。越老的代码,藏的坑可能越多。建立定期的补丁更新机制,做好纵深防御,比什么都重要。


你线上的NGINX配置里有没有多段rewrite/set链式写法?排查后有没有踩中风险点? 你还遇到过哪些隐藏多年的基础组件经典漏洞?欢迎在评论区分享。