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

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 OverflowCVSS 4.0评分9.2严重等级引入版本NGINX 0.6.272008年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.spe-sp;le.bufe-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-capturesNULL||ne-ncaptures){return0;}// 直接返回原始长度完全不考虑is_args和转义膨胀returne-captures[2*n1]-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;ncode-n;if(e-capturesNULL||(size_t)ne-ncaptures){return;}pe-ip;lene-captures[2*n1]-e-captures[2*n];// 关键判断is_args为1就执行转义长度膨胀if(e-is_args){e-ipngx_escape_uri(e-ip,e-captures[2*n],len,NGX_ESCAPE_ARGS);return;}e-ipngx_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?versionv2 last; # 第二条指令使用匿名捕获组$1此时主引擎is_args已经是1 set $real_path $1; }执行第二条set指令的时候长度计算阶段会新建清零的子引擎lele.is_args 0按原始长度计算缓冲区大小。但真正写入的时候用的是主引擎ee-is_args已经被第一条rewrite置成了1所以会强制对$1的内容做URL转义。两条指令加起来刚好凑齐了“长度算少了写入写多了”的全部条件。这也是为什么大部分踩坑的配置都是链式写法——单条rewrite自己的内部逻辑还能保持一致跨指令之后状态直接脱节。主引擎e生命周期指令1rewrite带?设置e.is_args1指令2set $var $1长度计算新建lele.is_args0按原始长度分配缓冲区数据写入复用ee.is_args1执行URL转义内容膨胀堆缓冲区溢出很多运维写配置的时候根本不会意识到前后两条指令会有状态关联。毕竟从配置语法上看两条指令是独立的谁能想到一个问号的影响会顺着执行流往下传。三、触发条件精准判定三个核心前提缺一不可不是所有带问号的rewrite都会触发漏洞必须同时满足三个核心条件少一个都不行。条件1存在匿名正则捕获组只有$1、$2、$3这类按序号引用的匿名捕获组才会走前面说的有缺陷的代码路径。如果用的是命名捕获组比如(?path.*)对应$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命令就能打瘫一个站点。# 示例请求仅用于自测验证禁止用于非法攻击curlhttp://target.com/api/$(python3-cprint(*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-v21|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_COUNT0echo CVE-2026-42945 危险配置检测 echo扫描配置目录:$CONFIG_DIRecho-----------------------------------# 遍历所有.conf文件find$CONFIG_DIR-typef-name*.conf|whileread-rfile;do# 初筛匹配带问号且包含$数字捕获组的rewrite/set指令# 注跨指令链式风险需结合上下文人工确认risk_lines$(grep-nErewrite.*\?|\$[0-9]$file2/dev/null)ifecho$risk_lines|grep-qrewrite.*\?echo$risk_lines|grep-q\$[0-9];thenecho[!] 发现潜在风险文件:$fileecho$risk_lines|head-15echo-----------------------------------RISK_COUNT$((RISK_COUNT1))fidoneechoecho检测完成。echo提示本脚本为初筛工具跨文件include、嵌套block的链式风险需人工复核。把脚本保存为nginx_rift_check.sh加执行权限后运行指定你的NGINX配置目录即可。脚本做的是初筛能命中大部分明显的风险配置。对于跨多行、跨文件include的链式指令还需要人工核对。最稳妥的方式是直接升级版本一劳永逸。6.3 异常日志排查如果漏洞已经被触发过NGINX的错误日志里会留下明确的痕迹。执行这条命令排查最近7天的崩溃记录grep-isignal 11\|SIGSEGV\|worker process exited/var/log/nginx/error.log|tail-20如果频繁出现worker进程异常退出的记录并且时间和扫描流量吻合很可能已经有人在尝试攻击你的站点。6.4 无害验证方法如果你想确认自己的配置会不会真的触发溢出可以用少量特殊字符做测试不要直接用大长度载荷打崩服务。建议先在测试环境验证。# 低风险测试建议测试环境执行curl-vhttp://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?versionv2 last; set $real_path $1; # 安全配置改用命名捕获组 rewrite ^/api/(?real_path.*)$ /internal?versionv2 last; set $real_path $real_path;改完之后路由逻辑完全不变只是把按序号引用改成了按名字引用直接切断了漏洞触发路径。方法二拆分指令链隔离状态如果不想改捕获组写法也可以把带问号的指令和带捕获组的指令分到不同的处理阶段避免is_args标志跨指令传递。比如用内部跳转拆分两个处理步骤location /api/ { rewrite ^/api/(.*)$ /internal_api/$1 last; } location /internal_api/ { rewrite ^(.*)$ $1?versionv2 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链式写法排查后有没有踩中风险点你还遇到过哪些隐藏多年的基础组件经典漏洞欢迎在评论区分享。