漏洞
量好长度以后,NGINX 的变量变了——CVE-2026-42533 两遍求值失配始末
NGINX 先计算复杂字符串长度并按预算分配缓冲区,再执行复制;特定的正则 map 与捕获关系,或满足条件的不可缓存重写路径,会让两遍之间的字节发生变化,使未经认证的请求能够触发工作进程堆缓冲区越界,造成重启与拒绝服务,并在 ASLR 关闭或被绕过时带来代码执行可能。

文章导航
1 7 月 15 日同时修复两条发布线;范围判断先确认制品身份,而不是比较版本号大小
2026 年 7 月 15 日,NGINX 同时发布稳定版 1.30.4 与主线版 1.31.3。决定性事件并不只是两个版本号向前推进:两条发布线都加入了对工作进程堆缓冲区溢出的修复,而漏洞入口是一组特定关系——正则表达式 map、请求内共享的捕获状态,以及运行时字符串脚本的两遍求值。修复版 1.31.3 的官方变更记录说明,同一字符串表达式若先引用会被该映射改写的捕获,后引用映射结果变量,就可能触发缺陷;记录还单列了不可缓存变量的相似情形。修复提交 073ab5db06ec5f5079280a60a28f450b8f1ac504 中的 NGINX 1.31.3, docs/xml/nginx/changes.xml, release record, lines 8–28 保存了这段公开表述。它给出了调查起点,却没有单独解释 NGINX 为何会先量到一个值、随后复制另一个值,也不意味着任何含有 map 的安装都存在远程可达路径。
本文因此始终把范围拆成三个彼此独立的坐标:实际运行的是哪个发布制品,配置与请求路径能否激活状态迁移,以及每一项结论由哪一级证据支撑。版本决定受影响代码是否可能存在;配置决定两遍引擎能否观察到变化中的状态;流量决定未经认证的恶意构造 HTTP 请求能否让后一次取值超过前一次分配。丢掉其中任一坐标都会误判:可能仅因版本字符串较旧就把不可达服务器判为可利用,也可能因为数值更大的版本属于另一分支,就把确有可达路径的服务器误判为安全。
1.1 用标签、提交、树对象与分支修复点冻结发布事件
公开时间线非常集中。nginx.org 的 2026 年新闻页记录稳定版与主线版在 7 月 15 日同日发布;主线版变更记录和稳定版变更记录给出了等义的触发条件;NGINX Plus 同日发布 PLS.37.0.3.1 LTS 与 R36 P7;F5 作为 CNA 发布的记录时间为 2026-07-15T14:33:45.810Z。安全补丁序列也在当天通过 NGINX PR #1561 完成复核与合并。这些是厂商与项目公开事实。SOSEC 的仓库推导只有在把发布名称绑定到不可变 Git 对象,并逐行比较这些对象以后才开始。
存在漏洞的主线版基线是标签 release-1.31.2,它解析到提交 2fd01ed47a1fd2965754c83f53b33a789d0e07f1 和树对象 a316153a0ab959d1f855ef504baebd1fd0c702b1;其中 NGINX 1.31.2, src/core/nginx.h, nginx_version and NGINX_VERSION, lines 11–13 自报版本 1.31.2。已修复的主线版标签 release-1.31.3 解析到提交 073ab5db06ec5f5079280a60a28f450b8f1ac504 和树对象 6d5ca42a71655c0ba1cc17c0be336a43cd173515,新版本常量见 NGINX 1.31.3, src/core/nginx.h, nginx_version and NGINX_VERSION, lines 11–13。稳定版必须另行冻结:release-1.30.3 对应提交 47c3628d23efaa1bfb1a32afbe9e3d013f860c2c、树对象 dbf9e8205f1cde585c2de018aedd69585025e30c;release-1.30.4 对应提交 017cf98dcce217946572a896f0992370475e189f、树对象 296dce3d163a3a0b4151f5c8ab092defb4c32daf。标签便于人类阅读,提交与树对象则使逐行结论不受网页界面或移动分支变化影响。
| 冻结对象 | 不可变身份 | 在本次调查中的作用 | 单凭它不能证明什么 |
|---|---|---|---|
release-0.9.6,2011-03-21 | 57f3455ad3d5e… | 首个包含正则映射支持的版本;与 nginx.org 的历史受影响下界一致 | 不表示从 2011 年起的每一种配置都能到达溢出 |
| 引入正则映射的提交 | 0519b43a7724… | 仓库历史将该功能描述为允许用正则表达式作为映射参数 | 功能引入不等于已经演示当代利用方法 |
release-1.30.3 | 47c3628d23ef…;树对象 dbf9e820… | 存在漏洞的稳定版基线 | 下游软件包保留该可见版本时,仍可能带有另行证明的等价回移 |
release-1.30.4 | 017cf98dcce2…;树对象 296dce3d… | 首个修复该漏洞的稳定版 | 它不是主线版或嵌入式产品可以共用的数值下界 |
release-1.31.2 | 2fd01ed47a1f…;树对象 a316153a… | 本文执行链使用的存在漏洞的主线版基线 | 标签能证明代码身份,不能证明本地可达性或已经发生利用 |
release-1.31.3 | 073ab5db06ec…;树对象 6d5ca42a… | 首个修复该漏洞的主线版,也是逐行对照目标 | 不能证明旧工作进程或第三方直接执行脚本的调用者已经采用新边界 |
详细 HTTP 执行跟踪选用相邻主线标签 1.31.2 与 1.31.3,再独立核验稳定版 1.30.4 是否携带同一修复族,不能只凭相似的发布说明推断代码相同。PR #1561 中显示的短哈希也必须与合入发布历史后重写的提交分开。CVE-2026-42533 在主线发布历史中的相关序列依次为:准备性的捕获复制简化 28219209e0b4…、标准复制操作的主要边界保护 b767540492e8…、访问日志路径的保护传播 4d32a2703c79…、直接脚本消费者的传播 25f920eca977…,以及按实际输出长度收口的 a8289aa69c74…。稳定版对应提交与主线版具有逐项相同的补丁标识;这比“变更记录文字相同”更有力地证明回移等价。
这条边界还能阻止常见的错误归因。1.31.3 同时包含 0cca8e055a2d…,它修复与 CVE-2026-60005 有关的陈旧正则捕获;还包含 700dc9e0e750…,它修复与 CVE-2026-56434 有关的子请求重复结束问题;更早的编译器边界提交 42f8df65b694… 同样无关。多个改动靠近同一标签或拉取请求,并不会自动成为同一漏洞的组成部分。下文逐行机制只把两遍尺寸计算与复制边界变化归入本报告,绝不把 1.31.2 到 1.31.3 的整份差异都称作 CVE-2026-42533 的修复。
相邻标签让主线比较保持受控。从 1.31.2 到 1.31.3 不会把数年无关重构混入执行器跟踪;另做 1.30.3 到 1.30.4 的比较,则检验稳定版是否获得同一安全约束。这种方法也能暴露遗漏:发行商若只报告通用复杂值的保护,而其构建仍暴露未设置末端或未检查错误的改写执行器、访问日志路径或直接脚本消费者,证据就还不能结案;反过来,完整的逻辑回移即使仍显示 1.30.3,也可能真正关闭缺陷。问题从来不是“这个字符串是否按字典顺序大于 1.30.4”,而是“该可执行文件来自哪条谱系,且本地所有可达路径是否都具备完整修复契约”。
1.2 稳定版、主线版、Plus 与嵌入式产品各自有独立修复目标
nginx.org 安全公告总表将 CVE-2026-42533 标为 major,给出从 0.9.6 到 1.31.2 的历史受影响范围,并把 1.30.4+ 与 1.31.3+ 列为不受影响。这里的两个加号分别属于两条发布线。稳定版 1.30.4 即使数值低于 1.31.0,也已经包含回移;主线版 1.31.0 和 1.31.1 不会因为普通语义版本比较把它们排在 1.30.4 之后,就自动变得安全。当前稳定版 1.30.0 至 1.30.3 应升级到 1.30.4 或后续稳定版,主线版直到 1.31.2 应升级到 1.31.3 或后续主线版。1.28、1.26 等旧分支仍位于公开历史范围内,却没有获命名的旧分支修复点,因此应迁移到受支持的修复发布线,或采用能够独立证明等价修复的发行商构建。
原始 F5 CNA CVE 记录把开源版编码为两个自定义区间:0.9.6 ≤ v < 1.30.4 与 1.31.2 ≤ v < 1.31.3。这种机器可读形状并不等同于 nginx.org 面向人的历史表述,尤其不能把第二个区间反推成“1.31.0 与 1.31.1 安全”。两个来源服务不同的表示需求,任何一个都没有允许调查者丢掉分支身份。资产清点必须先确认制品属于上游稳定版、上游主线版、NGINX Plus、发行版软件包还是嵌入式下游镜像,再为该谱系选择相应修复点。
| 产品或发布线 | 公开受影响口径 | 厂商或项目记录的修复候选 | 验收边界 |
|---|---|---|---|
| NGINX OSS 稳定版 | 当前稳定版 1.30.0–1.30.3;历史范围始于 0.9.6 | 1.30.4 或后续稳定版 | 证明分支身份或等价回移,不能套用一个线性比较器 |
| NGINX OSS 主线版 | 历史发布直到 1.31.2 | 1.31.3 或后续主线版 | 不得根据 CNA 区间形状推断 1.31.0 或 1.31.1 安全 |
| NGINX Plus 37 LTS | 37.0.0.1–37.0.2.1 | PLS.37.0.3.1 LTS | 记录完整 Plus 制品,不能只写“R37” |
| NGINX Plus 旧发布模型 | R33–R36 | R36 P7 或受支持的修复发布线 | 公告没有为 R33–R35 分别列出修复候选 |
| NGINX Instance Manager 2.x | 2.17.0–2.22.1 | 该发布线无候选 | “None”表示需要迁移,不表示不受影响 |
| F5 WAF for NGINX 5.x | 5.9.0–5.13.3 | 5.13.4 | 更新产品制品,而不只是宿主机上的 nginx 软件包 |
| NGINX App Protect WAF 5.x / 4.x | 5.2.0–5.8.0 / 4.11.0–4.16.0 | 这些发布线无候选 | 迁移到包含修复的受支持产品版本 |
| NGINX Gateway Fabric 2.x | 2.0.0–2.6.6 | 2.6.7 | 核对部署真正选用的控制器与镜像摘要 |
| NGINX Gateway Fabric 1.x | 1.3.0–1.6.2 | 该发布线无候选 | 必须迁移,不能把不受支持的发布线当作例外 |
| NGINX Ingress Controller 5.x LTS | 2026-lts-r1–r3 | 2026-lts-r4 | 以完整 LTS 制品名作为验收单位 |
| NGINX Ingress Controller 5.x | 5.0.0–5.5.2 | 5.5.3 | 确认正在运行的工作负载确实拉取了修复镜像 |
| NGINX Ingress Controller 4.x / 3.x | 4.0.0–4.0.1 / 3.5.0–3.7.2 | 这些发布线无候选 | 改用受支持的修复发布线;无候选不是豁免 |
下游各行来自 F5 公告 K000162097;公告在 7 月 17 日更新,本次源审在 7 月 20 日冻结其内容。它们描述的是经过评估的产品制品,不是从嵌入式 nginx -v 字符串到产品结论的通用映射。控制器、WAF 或管理产品可能封装自己的核心、模块、基础镜像和升级流程;脱离产品流程替换 nginx.exe 或宿主机软件包,可能根本没有改变实际制品。反过来,发行版也可能保留较旧的可见版本号,却已经回移完整逻辑修复。两种情况都要用软件包或镜像来源、补丁证据与构建证明验收,而不能依据响应中的 Server 头,也不能依据只写一个 CVE 编号的工单。
F5 的评估范围明确排除了已经结束技术支持的版本。在受支持范围内,公告把 BIG-IP Next SPK、BIG-IP Next CNF、BIG-IP Next for Kubernetes、BIG-IP 全部模块、BIG-IQ Centralized Management、F5 Distributed Cloud 全部服务、F5 Silverline 全部服务、NGINX One Console、F5OS-A、F5OS-C、NGINX“其他全部产品”、Traffix SDC 与 F5 AI Gateway 列为不受影响。这个列表应得到尊重,不能因为品牌相同就把漏洞扩大到每个产品;但也不能把结论向后外推至 F5 没有评估的已终止支持版本。“未列为受影响”“在评估范围内明确不受影响”和“处于评估范围外”是三个不同的资产结论。
公开攻击表述与代码维护范围之间还有一道边界。F5 与 CNA 描述的是恶意构造的 HTTP 请求和纯数据面问题,并未声称控制面暴露。上游修复同时加固了对应的 stream 脚本引擎:存在漏洞的 1.31.2 中,NGINX 1.31.2, src/stream/ngx_stream_script.h, ngx_stream_script_engine_t, lines 17–29 所示引擎有 ip 与 pos,却没有目标缓冲区末端指针;修复版 1.31.3 在 NGINX 1.31.3, src/stream/ngx_stream_script.h, ngx_stream_script_engine_t, lines 17–31 增加 end 与状态字段。这种对称性是维护者把同一执行契约应用到两个子系统的仓库证据,却不是把厂商的 HTTP 攻击结论悄悄改写成“所有 stream 配置均可利用”的证据。
1.3 可达性是多项条件的合取;证据方法止步于未经验证的利用断言之前
应先逐字理解公开触发条件,再让源码分析细化它。修复版 1.31.3 的 NGINX 1.31.3, docs/xml/nginx/changes.xml, CVE-2026-42533 release entry, lines 20–26 要求 map 执行正则匹配,字符串表达式先出现会被该映射改变的捕获、后出现映射变量,并另列不可缓存变量。F5 记录还加上未经认证的恶意构造 HTTP 请求、攻击者无法完全控制的附加条件、工作进程堆缓冲区溢出与重启,以及仅在 ASLR 已关闭或可绕过时才可能发生的代码执行。F5 将弱点归为 CWE-122,并明确入口位于数据面而非控制面。这些事实要求优先处理,也禁止把结论简化成“默认 NGINX 普遍存在可靠的未认证远程代码执行”。
不可变的 1.31.2 源码给出了两组相关却不同的机制条件。映射变量在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 220–236 注册 ngx_http_map_variable() 作为取值函数,因此只声明而从未实际读取的映射,在请求运行时不会执行。处理函数在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 107–155 求出复杂来源并调用 ngx_http_map_find();后者在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2529–2586 先返回精确或哈希匹配结果,只有此前路径未决定结果才尝试正则。对于“捕获在前、映射在后”的分支,请求必须抵达受影响二进制、读取映射变量、避开更早决定结果的哈希项、命中会更新相关捕获的正则,再以捕获先于映射结果的顺序进入外层两遍字符串写入点,并使后一次写入长于先前预算。不可缓存的改写分支有另一组条件:不含正则的 volatile 映射若具有会变化的动态结果,也可由彼此分离的改写长度引擎和值引擎重新求值,无须执行正则或发布捕获便能产生失配。两条分支各自都是条件合取,任何单项都不能单独证明溢出。
| 判定条件 | 让资产继续进入候选的证据 | 会终止本文重建路径的条件 | 证据类别 |
|---|---|---|---|
| 制品 | 上游受影响标签,或缺少等价复制保护的下游构建 | 修复后的上游发布线或经独立证明的等价回移确实正在运行 | 厂商事实加制品检查 |
| 子系统 | 恶意构造的 HTTP 请求抵达数据面工作进程和相关虚拟主机 | 只暴露无关的管理或控制面服务 | F5/CNA 事实加部署拓扑 |
| 映射读取 | 请求路径真正读取映射输出变量 | 映射虽已声明,却在该路径从未读取 | 文档与源码数据流 |
| 捕获分支匹配器 | 正则项获胜,并创建或改变相关捕获状态 | 只有字面量或哈希项获胜、正则未匹配,或没有相关捕获;这会终止捕获分支,却不会终止另一条不可缓存改写分支 | 源码执行链 |
| 消费顺序 | 外层脚本先测量受影响捕获,后求映射输出 | 两个值从未以该顺序进入同一受影响写入点 | 源码指令顺序 |
| 重新计算路径 | 改写模块的长度引擎与复制引擎分别求不可缓存值 | 整次求值始终使用同一缓存结果,或第二次结果没有变化 | 官方条件加执行器差异 |
| 长度差 | 后一次实际字节多于前一次预测字节 | 后值等长或更短;这可能产生另一正确性问题,却不是本方向的越界写入 | 源码算术推导 |
| 流量影响 | URI、请求头、参数或其他受客户端影响的字段驱动匹配与增长 | 攻击者影响的数据无法进入该状态迁移 | 部署和配置证据 |
这张表是可达性模型,不是 SOSEC 已经执行利用的声明。本次源审阅读了官方公告与文档,把发布标签解析到不可变提交和树对象,对照存在漏洞的 1.31.2 与修复版 1.31.3、稳定版 1.30.3 与 1.30.4,跟踪编译器和运行时调用链,并核查公开修复提交;但没有运行本地利用程序,没有让工作进程崩溃,没有验证可用于实际攻击的请求,没有检查任何生产部署,也没有复核生产遥测数据。因此,对执行器机制和必要配置关系的描述属于仓库推导;影响与产品范围继续归因于 F5 和 nginx;关于某一组织是否已经遭到利用的断言,在配置、请求、进程与内存证据汇合前仍属未验证。
负面证据也应遵守同一纪律。对于“捕获在前、映射在后”的分支,映射未命中任何正则、相关表达式从未被消费、匹配没有改变所引用捕获,或后值没有变长,都会终止本文重建的这条路径。不过,不含正则或使用字面量键的映射并非天然安全:若它带有 volatile,且动态结果能够变化,彼此分离的改写长度引擎和值引擎仍可实例化不可缓存分支。排除其中一条分支,不等于整份二进制安全,也不能证明其他字符串写入点或漏洞不存在。ASLR 同样只改变 F5 对可能代码执行所列的附加条件,不会消除堆缓冲区越界或工作进程重启路径。范围只能逐项关闭:代码身份、分支特定的数据流、状态迁移或重新计算、长度增长与已观察后果。
2 请求进入两遍脚本引擎
HTTP 请求抵达这套机制时,NGINX 已不再解析配置文本。配置加载期间,每个动态字符串都已经翻译成一份可执行计划。请求提供变量表、正则捕获状态、内存池分配器与当前输入;计划则规定这些值按何种顺序测量、再按何种顺序复制。CVE-2026-42533 并不起于畸形配置语法。配置能够通过检查并完成编译,随后脚本引擎恰好按照自己生成的顺序执行它。
核心对象是 ngx_http_complex_value_t。存在漏洞的 1.31.2 在 NGINX 1.31.2, src/http/ngx_http_script.h, ngx_http_complex_value_t/ngx_http_compile_complex_value_t, lines 66–90 中保存原始值、待清理的变量索引表,以及名为 lengths 和 values 的两个不透明指令指针。一条指令流预测输出字节数,另一条对应的指令流真正发出这些字节。不含变量或捕获的字符串走字面量快速路径;含 $name、${name} 或 $1 至 $9 的字符串则变成两遍程序。修复版 1.31.3 在 NGINX 1.31.3, src/http/ngx_http_script.h, ngx_http_complex_value_t/ngx_http_compile_complex_value_t, lines 67–91 保留这套表示,这首先说明修复是在执行时强制边界,而非废弃原有编译设计。
这份程序有多个运行入口。ngx_http_complex_value() 自行掌管通用复杂值的两遍执行与内存分配;ngx_http_script_run() 把同一种安排开放给已经持有独立指令流、还可能预留定长附加字节的调用者;改写引擎又嵌入另一种形式,由 ngx_http_script_complex_value_code() 运行私有长度子程序、分配空间,再让外层改写指令流中的值指令填充缓冲区。三者都依赖一个共同前提:长度程序与值程序描述的是最终同一批字节。编译会在两条指令轨上保留同一词法顺序,因此这个前提看似自然,却没有快照支撑。长度指令可能解析变量,变量取值函数可能执行映射和正则,而这些工作会改变同一表达式其他位置随后读取的状态。
配置生命周期与请求生命周期只通过这份计划相遇。只要当前配置仍在使用,配置内存池便持有指令记录和字面量操作数。一个 HTTP 请求树会共享索引变量槽和请求内存池支撑;位置捕获字段通常属于各自的请求对象,但克隆子请求可以暂时别名父请求的位置状态。每次脚本求值都有自己的写入游标,修复代码中也有自己的目标边界;目标缓冲区本身却从请求树共享的内存池分配,并不归请求对象单独所有。因此,执行计划本身可以完全不可变,它每次观察到的值却仍会改变。两遍使用相同函数指针和操作数,只能证明选择了同一种读取操作,不能证明读取操作返回相同字节。这一区别排除了一个诱人的误诊:漏洞不要求字节码被破坏,也不要求另一线程并发改写指令流。指令流可以完好无损,而同一请求路径中的正常取值副作用仍能让先前预测失效。
2.1 编译器生成并行的长度字节码与值字节码
ngx_http_compile_complex_value() 是编译期入口。存在漏洞的 1.31.2 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_compile_complex_value(), lines 138–239 扫描来源字符串,分别统计普通变量与数字捕获,初始化输出对象;若没有动态引用便提前结束。否则,它在配置内存池中为 flushes、lengths 和 values 创建数组,把三者接入 ngx_http_script_compile_t,要求两条指令流都有完整终止项,然后调用 ngx_http_script_compile()。编译成功后,数组的底层存储就是运行时中间表示。修复版 1.31.3 在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_compile_complex_value(), lines 144–247 仍采用同一构造;该版本另加的 i + 1 < v->len 解析器加固与本 CVE 无关。
这里所说的“字节码”是线程化代码,并非由大型 switch 解释的紧凑数字操作码。存在漏洞的 1.31.2 在 NGINX 1.31.2, src/http/ngx_http_script.h, script opcode types and records, lines 89–115 分别声明值操作和长度操作的函数指针类型,随后定义由函数指针与操作数组成的记录;操作数可能是字节数、变量索引或捕获索引。运行时,引擎把 e.ip 指向的首个机器字解释为下一个函数,每个函数再按自身记录对齐后的大小推进 e.ip;空指针字终止一条指令流。编译器在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_compile(), lines 459–611 从左到右遍历表达式:$1 至 $9 选择捕获操作,标识符选择普通变量操作,其余字节组成字面量连续段;但若 compile_args=1,第一个问号会变成一对参数标记操作,由 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_add_args_code(), lines 990–1008 发出。依赖关系不会重排;来源若依次为捕获、分隔符、映射,两条指令轨都保留这个顺序。
对于一段字面量,存在漏洞的 ngx_http_script_add_copy_code() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_add_copy_code(), lines 808–849 向 lengths 追加 ngx_http_script_copy_len_code 和常量长度,再向 values 追加 ngx_http_script_copy_code、同一长度,以及按指针对齐的字面量字节。对于普通变量,ngx_http_script_add_var_code() 解析请求变量索引,把它加入 flushes,并在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_add_var_code(), lines 888–930 发出携带同一索引的长度记录和复制记录。命名正则捕获与映射输出都会编译成索引变量读取,但二者的生产者不同:映射槽由自身取值函数填充,NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_compile(), lines 2631–2667 则注册命名捕获,而 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2712–2737 会在正则执行的副作用中直接写入该槽。
位置捕获使用专门记录。存在漏洞的 ngx_http_script_add_capture_code() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_add_capture_code(), lines 1307–1339 保存 2 * n,因为 PCRE 用一对起止偏移表示每个分组;随后发出 ngx_http_script_copy_capture_len_code 与 ngx_http_script_copy_capture_code。它不像变量记录那样持有字符串指针,每次运行时操作码都会查询请求当前的捕获偏移和数据基址。最后,ngx_http_script_done() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_done(), lines 725–767 向完整的长度程序和值程序写入空函数指针。这些空值是指令边界,不是目标缓冲区边界:它们说明何时停止取指,却没有说明已取出的指令最多可以写多少字节。
| 来源片段 | 长度指令轨 | 值指令轨 | 查询的运行时状态 | 稳定性条件 |
|---|---|---|---|---|
| 字面量字节 | copy_len 返回内嵌计数 | copy 发出内嵌字节 | 仅查询指令记录 | 其自身不可变 |
| 普通变量、映射输出或命名捕获 | copy_var_len 解析索引并返回当前 len | copy_var 再解析同一索引,复制当前 data/len | r->variables[index] 及其取值函数 | 只有相关副作用和允许的重新计算均未介入才稳定 |
位置捕获 $1 至 $9 | copy_capture_len 读取当前偏移对 | copy_capture 重读偏移与捕获数据基址 | r->captures、r->ncaptures、r->captures_data | 只有后续正则没有替换请求捕获状态才稳定 |
| 经过转义的位置捕获 | 统计原始字节和转义扩张 | 转义当时的捕获切片 | 捕获向量、基址、URI 标志与实际字节 | 切片和编码后长度都必须保持相同 |
| 终止项 | 空函数指针 | 空函数指针 | 指令指针 | 停止分派,但不限制输出游标 |
改写模块的编译器说明,两条指令轨并不一定分别占据完整、独立且并行的数组。存在漏洞的 1.31.2 中,ngx_http_rewrite_value() 为 ngx_http_script_complex_value_code_t 创建私有长度数组,却把 sc.values 指向该 location 的更大改写代码数组,并且只要求长度程序的终止项;精确实现见 NGINX 1.31.2, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_value(), lines 965–1019。修复版 1.31.3 保留这种拆分:NGINX 1.31.3, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_value(), lines 1007–1014 初始化编译器并设置 complete_lengths,NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_done(), lines 765–771 发出私有空终止项,而 NGINX 1.31.3, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_value(), lines 1020–1027 另行追加外层复杂值结束记录。复制操作仍会顺序落到该记录,再继续执行后续改写指令。因此,准确解释必须点明具体执行器,不能笼统声称所有复杂值都具有完全相同的缓存行为。
可以用来源顺序 $1|$route 观察编译结果,其中 $route 是一个映射输出,获胜的正则能够替换第 1 个捕获。长度程序在概念上依次是 capture_len(2)、copy_len(1)、var_len(route_index),最后为空指针;值程序依次是 capture_copy(2)、copy("|")、var_copy(route_index),最后同样为空指针。加倍的操作数指向第 1 个分组的起止偏移对,映射索引则指向请求变量槽。长度遍先由第一个操作测量旧偏移对,最后一个操作可能启动惰性取值函数并安装新偏移对;值遍重新开始后,第一个操作读到的已经是新偏移对。编译器完整保留来源顺序,也为每个片段选择了正确操作族;结果仍不安全,因为这种配对只是时间上的对应,并非同一事务中的快照。
记录对齐影响指令解码,却不改变容量结论。每个操作都按自身记录和内嵌数据对齐后的大小推进 e.ip,因此源码复核者应跟随记录布局,而不是在字节数组中搜索数字操作码。错误的推进确实可能分派到错误函数,但本文重建的不是那条路径。每次推进都可以正确,每个空终止项都可以存在,目标分配仍然可能偏小。指令完整性与输出容量完整性是两项独立约束。
编译期快速路径进一步收窄候选范围。若扫描没有发现变量或捕获,原值保持为字面量,不需要两条请求期指令轨;若发现动态部分,生成的记录会保留其词法依赖,编译器既不会按取值关系重新排序,也不会为将来读取的来源建立快照。静态复核因此能够从每个复杂值的有序片段推导候选顺序,却必须解析每个索引变量究竟代表什么。普通变量、命名捕获与映射输出可以使用相同的变量记录形状,只有后两类可能隐藏正则产生的状态。只匹配文本中的 $name 不足以判定风险,还要把索引连接到已注册的取值函数和真正生产者。
修复版编译器新增的改写结束操作,与原有空终止项并非一回事。空终止项通过停止指令分派来结束私有长度程序或独立值程序;改写结束操作则位于更大的外层指令流中,在最后一个复杂值复制操作之后真正执行,并在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_complex_value_end_code(), lines 1873–1884 按实际游标收口所生成的字符串,再让外层解释器继续。即使值记录仍与后续改写指令交织,它在表达式边界的位置也为嵌套分配划出了明确作用域。正因如此,回移审计必须分别检查初始化这种拆分的编译器、发出私有空终止项的通用编译器、发出外层结束记录的改写编译器,以及消费该记录的运行时实现。
配置内存池的容量并不是请求运行时的输出预算。普通复杂值编译会在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_compile_complex_value(), lines 182–205 预先初始化数组;NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_init_arrays(), lines 682–721 也只为传入空数组的编译器调用者估算容量。后续追加的代码仍可经 NGINX 1.31.2, src/core/ngx_array.c, ngx_array_push_n(), lines 95–140 扩容或迁移。这些配置期分配保护的是线程化指令,并非请求输出;其中尚未使用的对齐字节不会变成请求目标游标之后的余量。复核记录应分开标注两个分配域,而不是比较它们的字节数。
清理程序是第三种编译产物,其成员规则也不同。NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_compile_complex_value(), lines 138–239 会为动态表达式创建局部清理数组,但只有普通变量引用追加索引后,才会把运行时列表挂到结果上;完整列表最后再追加 (ngx_uint_t)-1。仅含数字捕获的值不会发布 flushes 指针,因为捕获操作码直接读取请求字段;命名捕获被引用时则会进入列表,因为它编译成索引变量。这个运行时列表描述哪些槽位可以失效,并不枚举两条指令轨观察到的所有可变依赖。
2.2 运行时严格经过清理、长度计算、精确分配与复制
最紧凑的运行时实现是存在漏洞的 1.31.2 中的 ngx_http_complex_value(),见 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_complex_value(), lines 57–104。字面量会直接返回;动态值先调用 ngx_http_script_flush_complex_value(),清零引擎,把 e.ip 指向 lengths,挂接当前请求,并设置 e.flushed = 1。随后它逐个调用长度函数,把返回值累加为一个 size_t len,把预测值写入结果,再调用 ngx_pnalloc(r->pool, len) 从请求内存池精确分配这么多字节。分配完成后,函数把 e.ip 切到 values,令 e.pos 指向分配首字节,并从表达式起点重新分派复制函数。两遍之间没有增长余量、自动重试或私有值快照。
入口处的清理既不是快照,也不是第二遍边界。存在漏洞的 ngx_http_script_flush_complex_value() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_flush_complex_value(), lines 35–54 遍历编译得到的索引,只为已经标记 no_cacheable 的槽位清除 valid 与 not_found,并不会复制变量字符串或捕获偏移。由于 e.flushed = 1,普通长度操作和值操作都使用 ngx_http_get_indexed_variable();它在槽位有效时直接返回,槽位为空时才运行取值函数。另一条 ngx_http_get_flushed_variable() 会先让有效但不可缓存的槽位失效,再进入索引取值路径;两者实现均见 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_get_indexed_variable()/ngx_http_get_flushed_variable(), lines 618–685。所以普通 ngx_http_complex_value() 通常只在入口清理后计算一次 volatile 映射。第一类公开漏洞并不是两遍必然重算该映射,而是长度程序靠后的映射取值函数改写了前面已经测量的捕获。
取值函数在所谓“计算长度”期间可以执行大量逻辑。映射输出的取值函数要先计算来源、执行查找,还可能继续求动态结果;正则匹配同时会改变请求捕获状态。因此,长度遍沿着一串状态前进,而不是观察一个冻结瞬间。令 S0 表示入口状态,早期长度操作观察 S0;靠后的映射操作调用取值函数,把请求推进到 S1;最终分配却是沿途不同时刻测量结果的总和。值程序随后从第一条指令重新运行,看到的是 S1 而不是 S0。即使没有任何并发,语法上并行的两条指令轨也已经产生语义差异。
存在漏洞的 ngx_http_script_run() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_run(), lines 614–660 把同一种计数方式开放给直接调用者。它先让当前所有不可缓存变量失效,设置 e.flushed = 1,把长度操作返回值加到调用者预留的固定后缀字节数上,精确分配总量,再让值程序写入动态部分并返回其游标。修复版 1.31.3 保留这个顺序,却在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_run(), lines 623–678 设置 e.end = value->data + n 并传播复制状态。最终的 value->len = e.pos + len - value->data 表示动态部分实际游标距离加上调用者预留的后缀,而不只是动态指令轨已经发出的字节数。差异表明精确分配本身仍是合理优化;真正改变的是不再无条件信任可变预测。
不可缓存变量的第二类路径属于改写执行器,绝不能泛化到上面每次调用。存在漏洞的 ngx_http_rewrite_handler() 在 NGINX 1.31.2, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_handler(), lines 137–183 分配清零后的外层引擎,让 flushed 保持为假。当它执行 ngx_http_script_complex_value_code() 时,该操作码又建立一份清零后的长度引擎 le,运行私有长度程序,按结果总和精确分配,再把缓冲区和游标交给外层引擎;实现见 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_complex_value_code(), lines 1759–1799。长度操作与稍后外层复制操作因而都会选择 ngx_http_get_flushed_variable()。被标为不可缓存的映射可以在量长度时运行一次,在复制字节时再运行一次。修复版 1.31.3 在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_complex_value_code()/ngx_http_script_complex_value_end_code(), lines 1834–1884 增加目标边界与结束操作。
| 执行入口 | 谁负责长度遍 | 值遍所在环境 | 相关路径如何选择变量取值函数 | 对应的漂移类型 |
|---|---|---|---|---|
ngx_http_complex_value() | 函数在一次编译索引清理后自行执行 | 同一引擎以 flushed=1 重新运行 | 两条指令轨都使用索引取值函数 | 靠后的长度操作可改变前面已经量过的捕获状态 |
ngx_http_script_run() | 函数自行执行,并加入调用者给定的定长字节 | 同一份已初始化引擎从值程序起点重启 | 入口失效处理后使用索引取值函数 | 同属演化状态;直接调用者还必须得到并传播边界 |
| 改写模块复杂值 | 清零后的私有引擎 le | 清零后的外层改写引擎继续执行内嵌值记录 | 两份引擎的 flushed 都为假,故均使用刷新式取值函数 | 不可缓存值可在测量与复制之间重算并增长 |
普通复杂值的顺序可以逐条指令审计。入口只让符合条件的已编译槽位失效一次;引擎从首条长度记录开始;捕获长度记录读取当前偏移对;字面量长度加入常量;靠后的变量长度记录抵达尚未缓存的映射取值函数,获胜正则改变请求捕获状态,并使返回的映射值变为有效;循环结束后,内存池分配器按精确总和分配;同一引擎再以 flushed=1 从首条值记录重启;捕获复制读取新偏移对,字面量复制推进游标,映射复制取得已经缓存的结果。没有哪个操作单独乱序,映射也不需要运行两次。分歧只因为第一次捕获读取发生在唯一一次状态改变之前,第二次捕获读取发生在其后。
改写模块路径在两个精确位置不同。其私有长度引擎经过清零,并未设为 flushed=1;执行复制记录的外层改写引擎也是另一份独立清零的对象。两条指令轨上的变量操作都会调用刷新式取值函数。一旦第一次结果带上不可缓存标志,第二个取值函数会先令其失效,再次调用处理函数。如果第二次求值选择了更长的动态结果,即使表达式前面没有捕获片段,映射变量本身也会产生正长度差;若它同时改写了别处消费的捕获,两项长度差还可能叠加。但仍不能把结论写成“volatile 总会运行两次”:它只适用于这种执行器形状,且变量必须被实际读取并标为不可缓存。
精确分配本身并非编程错误;把预测当成写入许可才会使它不安全。在每次复制 n 字节之前,写入者至少要满足二者之一:持有不可变证明,确认目标还剩不少于 n 字节;或针对即将修改的分配,实际判断 n <= end-pos。存在漏洞的 1.31.2 对这些可变读取都没有相应保证。修复设计选择第二项,让求值仍可保持惰性、状态仍可变化,同时把低估长度转成受控错误。这既保留正常情况下两遍设计的性能与语义,也把信任基础从“两遍理应一致”改为“本次写入必须装得下”。

不含美元引用的改写值不会进入嵌套两遍路径。存在漏洞的 NGINX 1.31.2, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_value(), lines 965–1019 在 ngx_http_script_variables_count() 返回零时,只发出一条 ngx_http_script_value_code 记录;只有动态值才会得到 ngx_http_script_complex_value_code、私有长度数组和内嵌值操作。因此,常量改写字符串并不是检验 volatile 类别的合格负面对照,因为它直接移除了待测执行器。有效对照应保留动态引用,只改变可缓存性或状态迁移,从而维持被比较的执行契约。
取值函数失败还有另一条路径,可能伪装成稳定的空值。注册的处理函数若未返回 NGX_OK,存在漏洞的 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_get_indexed_variable(), lines 647–664 会把槽位标成无效且未找到,再返回空指针。NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_var_len_code()/ngx_http_script_copy_var_code(), lines 933–987 中的变量长度操作对“未找到”槽贡献零,变量复制操作也不发出任何字节。只有日志和哨兵已经证明取值函数成功完成时,零长度观察才构成有用的负面证据;否则,调查者只是把被抑制的错误误认成稳定。
2.3 字面量、变量与捕获操作码都继承了“没有目标缓冲区上界”的旧契约
缺少的强制约束直接写在存在漏洞的 1.31.2 的 ngx_http_script_engine_t 中。NGINX 1.31.2, src/http/ngx_http_script.h, ngx_http_script_engine_t, lines 17–36 包含指令指针 ip、写入游标 pos、栈指针、工作字符串、标志、状态与请求指针,却没有分配区末端指针。e.buf 也不是通用替代物,因为部分执行器并未初始化它,标准复制操作也从不据此计算剩余容量。修复版 1.31.3 在 NGINX 1.31.3, src/http/ngx_http_script.h, ngx_http_script_engine_t, lines 17–37 的 pos 与 sp 之间加入 u_char *end。仅这一项结构对照就足以否定“旧复制操作存在某个隐含通用上界”的说法。
字面量操作对能够展示写入点契约,但它自身不会制造漂移。存在漏洞的 ngx_http_script_copy_len_code() 返回内嵌常量;ngx_http_script_copy_code() 随后在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_len_code()/ngx_http_script_copy_code(), lines 852–885 用 ngx_copy() 复制同样数量的内嵌字节并推进 e.pos。这项计数不可能变化,但复制操作仍假定此前总和已经为它留出空间。修复版 1.31.3 保留同一操作对,并按 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_copy_len_code()/ngx_http_script_copy_code(), lines 889–927 所示逻辑在复制前检查容量。
普通变量操作对则两次读取活动状态。存在漏洞的 ngx_http_script_copy_var_len_code() 根据 e.flushed 解析索引槽并返回当前 len;ngx_http_script_copy_var_code() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_var_len_code()/ngx_http_script_copy_var_code(), lines 933–987 再次解析同一索引,把当前 data/len 交给 ngx_copy()。记录既不保存第一次长度,也不保存第一次数据指针;“同一索引”被当成了“同一批字节”。修复版 1.31.3 仍以相同方式读取,却在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_copy_var_len_code()/ngx_http_script_copy_var_code(), lines 975–1034 复制前检查当前长度。
位置捕获绕过变量表,却重复了同一假设。存在漏洞的 ngx_http_script_copy_capture_len_code() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_capture_len_code(), lines 1342–1377 相减当前起止偏移,并在需要时加入转义扩张;稍后的 ngx_http_script_copy_capture_code() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_capture_code(), lines 1380–1417 重读两端偏移和 r->captures_data,然后复制或转义当时的切片。它只检查捕获此刻是否存在,并不检查它是不是先前测量过的那个捕获。转义也无法恢复稳定性:切片一旦改变,原始字节数与需要百分号展开的字节数都可能变化。
命名捕获由成对的普通变量操作读取,因为正则编译会把分组注册到变量索引。表示差异并不会创造快照:正则执行可以在值操作重新访问槽位前覆盖命名捕获。反过来,字面量、保持稳定的缓存变量,或始终逐字节相同的捕获,并不会仅因使用这套引擎就产生漂移。缺陷必须同时存在可变状态观测与长度增长。旧终止项无法防护,因为它限制的是指令数量而非输出字节;旧 status 字段也无法自动防护,因为这些复制操作在写入前没有任何条件会把它设为错误。
修复版 1.31.3 在 NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_check_length(), lines 827–842 的 ngx_http_script_check_length() 中把缺失的判断显式化。end 为空时,函数保留源码层面可选边界的调用约定;否则,它比较剩余容量与本次待写长度。空间不足时,函数发出通用告警,把指令指针转到退出操作码,设置内部错误状态并返回 NGX_ERROR。NGINX 1.31.3, src/http/ngx_http_script.c, ngx_http_script_copy_capture_code(), lines 1439–1489 中的捕获写入操作会调用这项检查,但它本身并不建立边界。标准封装器分别通过 ngx_http_complex_value(), lines 58–110 和 ngx_http_script_run(), lines 623–678 建立边界并向调用者传播受限执行结果。lines 1834–1884 中的嵌套改写运行时与结束操作码会建立该嵌套值的边界并按实际长度收口;若复制失败,则由边界检查设置退出指针和错误状态,指令轨停止后,ngx_http_rewrite_handler(), lines 137–183 再返回引擎状态。存在漏洞版本的结论不变:两条指令轨读取不断演化的请求状态,精确分配没有余量,而 1.31.2 的写入操作只有游标,没有目标缓冲区末端指针。
3 惰性求值的 map 在长度程序完成测量后改写捕获
公开触发条件指向一种顺序关系,而不是正则表达式与 map 只要同时存在就会触发漏洞。修复版 1.31.3 的发布记录在 NGINX 1.31.3, docs/xml/nginx/changes.xml, CVE-2026-42533 release entry, lines 20–26 说明,受影响字符串先放置被映射改变的捕获,再放置映射结果,并另列不可缓存变量这一类。官方 ngx_http_map_module 文档说明了预期行为:映射变量只有在实际使用时才求值;正则项可以发布命名捕获或位置捕获;结果可以是复杂字符串;volatile 会把结果标为不可缓存。任何一项单独都不是缺陷。下面的仓库执行链说明这些正常特性如何与受影响引擎的精确分配、无边界复制结合。
状态迁移发生在同一工作进程处理的同一请求内。长度程序先读取一个捕获;靠后的映射取值函数运行正则并发布另一次匹配;值程序随后重新读取捕获。这里不需要第二线程、时序竞态,也不需要 PCRE 返回非法边界。共享对象是当前请求的正则状态,同一请求内的多条脚本指令都能观察它。修复版 1.31.3 有意保留惰性映射及其捕获副作用:映射处理函数和查找链在实质上没有变化,复制操作获得的则是目标缓冲区边界。这个对照把失效契约定位在从预测到复制的强制约束,而不是合法的映射语义。
3.1 改写状态的调用链依次是 map 变量取值、map 查找与正则执行
配置加载时,存在漏洞的 1.31.2 创建输出变量,并在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 175–236 把 ngx_http_map_variable() 安装为其取值函数。注册取值函数正是惰性求值的基础,注册动作本身不会执行匹配。请求第一次索取输出时,ngx_http_get_indexed_variable() 在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_get_indexed_variable(), lines 618–665 返回已经缓存的变量槽,若槽位为空才调用注册的处理函数。因此,第一次读取可以恰好发生在长度程序中映射变量所在的位置,而排在它前面的捕获长度指令已经返回旧尺寸。
存在漏洞的 ngx_http_map_variable() 先把映射来源作为复杂值求出,在主机名模式下去除末尾句点,再调用 ngx_http_map_find();随后选择默认结果,或在选中值为动态值时继续计算复杂结果,完整路径见 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 107–155。映射结果是常量,并不会让正则分支失去副作用:选择结果之前仍会运行正则并改变捕获,然后才返回常量。相反,动态结果会引入嵌套求值,但它不是普通“捕获在前、映射在后”这一类路径的必要条件。
查找顺序清楚写在存在漏洞的 ngx_http_map_find() 中,见 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2529–2586。来源非空时,函数分配一份小写副本作为组合哈希键;来源为空时,该副本保持空指针,但函数仍会调用组合查找。NGINX 1.31.2, src/core/ngx_hash.c, ngx_hash_find_combined(), lines 211–245 先尝试精确哈希;若键为空,则在通配符查找之前返回。精确项、通配符项或主机名项一旦命中,函数便直接返回,不会执行任何正则;只有非空来源在整套组合哈希结构中完全未命中时,才会按配置顺序进入正则。解析器在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 527–562 单独识别 ~ 和 ~* 并编译正则。扫描器必须保留查找优先级与可达分支,不能只标记每个含波浪号的映射。
命名捕获由另一种生产者写入。存在漏洞的 ngx_http_regex_compile() 在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_compile(), lines 2600–2670 记录捕获元数据,把每个名称注册成可变的索引变量,将其索引与 2 * group_number 配对,并先为它安装通用的“未找到”取值函数。匹配成功后,ngx_http_regex_exec() 会在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2712–2737 直接索引 r->variables 并写入每个命名槽。状态所有权是分层的,不能简单概括为“请求局部”:顶层请求在 NGINX 1.31.2, src/http/ngx_http_request.c, ngx_http_alloc_request(), lines 630–637 分配索引变量数组,而每个子请求都在 NGINX 1.31.2, src/http/ngx_http_core_module.c, ngx_http_subrequest(), lines 2502–2509 指向父请求的同一数组。位置捕获字段则属于各自的请求对象:普通子请求通过 ngx_http_subrequest(), lines 2423–2426 的 ngx_pcalloc() 取得清零对象;克隆子请求才会暂时别名父请求的捕获计数、向量和数据指针,直到带捕获的正则按 ngx_http_subrequest(), lines 2556–2572 与 ngx_http_regex_exec(), lines 2683–2694 重新分配捕获向量为止。
执行器只在需要时准备捕获向量,并非所有正则都走同一路径。NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2673–2739 显示,带捕获的正则会分配或复用请求捕获向量;不含捕获的正则则以零长度向量调用 PCRE。未匹配时,函数在发布成功状态之前返回 NGX_DECLINED;匹配成功后,它先更新所有已注册命名槽,再发布位置捕获计数与来源指针。不同 PCRE 后端的处理方式也不同:PCRE2 构建中的 NGINX 封装器会在 NGINX 1.31.2, src/core/ngx_regex.c, PCRE2 ngx_regex_exec(), lines 390–451 取得库的偏移向量,再把偏移复制进 NGINX 的整数捕获数组;PCRE1 构建则由 the PCRE1 wrapper, lines 453–460 把该整数数组直接交给 pcre_exec() 填充。无论采用哪种后端,没有参与匹配的可选命名分组都会留下 -1, -1 偏移对,两者相减得到合法的零长度命名值,却不代表确有匹配切片。捕获字节从未复制到表达式私有的快照中。
普通可缓存映射的完整过程可以确定地分成八步:更早的操作留下不存在、为空或较短的捕获;外层长度程序读取它;靠后的映射输出尚未缓存;其取值函数计算来源;哈希查找未命中而某个正则项获胜;正则执行发布更长捕获;映射结果的长度完成预算并被缓存;值程序从头运行,先读取新捕获,随后再复制已经缓存的映射结果。某条路径若更早计算过该普通映射,后续读取可能直接命中缓存,从而避免这次较晚发生的副作用。但这不是通用缓解措施,因为另一请求阶段、位置、内部跳转、错误路径或请求形状仍可能先抵达候选表达式。静态分析只能找出潜在顺序,路径感知测试才判断首次求值是否真正可达。
修复版 1.31.3 在 NGINX 1.31.3, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 107–155 保留 ngx_http_map_variable(),也在 NGINX 1.31.3, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2529–2586 保留 ngx_http_map_find()。存在漏洞的源码中还可见对应的 stream 调用链:NGINX 1.31.2, src/stream/ngx_stream_map_module.c, ngx_stream_map_variable(), lines 105–152、NGINX 1.31.2, src/stream/ngx_stream_variables.c, ngx_stream_map_find(), lines 977–1030,以及 NGINX 1.31.2, src/stream/ngx_stream_variables.c, ngx_stream_regex_exec(), lines 1122–1186。这种并行实现足以支持维护者审计 stream 脚本;公开 CVE 的攻击结论仍限于恶意构造的 HTTP 请求,不能越过这道证据边界。
查找优先级形成四种不同结果。组合哈希中的精确项、通配符项或主机名项一旦命中,函数便会在任何正则运行前返回。哈希未命中后,空来源会跳过正则循环并进入默认值;非空来源则按配置顺序遍历正则,第一条成功项选择结果并发布匹配状态。若所有正则都返回未匹配,ngx_http_map_find() 不会返回值,ngx_http_map_variable() 因而选择默认值。最后这种默认路径并不是不含正则的哈希快速返回:能够稳妥下结论的只有“本次查找没有发布成功匹配”,不能据此断言此前捕获状态不存在或不可变。测试与扫描器必须保留这些分支,不能把所有默认结果或所有含波浪号的映射视为同一种情况。
成功正则有两个输出,不能混为一谈:它一方面选择返回给映射输出变量的结果,另一方面把匹配状态发布到请求中。选中的结果可以是常量,因此结果计算本身很简单,捕获变化却仍有意义。映射输出随后可以被缓存为有效;这会阻止普通索引路径再次运行同一取值函数,却不会恢复先前捕获。值程序因而可以复制已经缓存的 5 字节映射结果,同时重新读取为了选择该结果而产生的 11 字节捕获。缓存让首次求值后的映射返回值稳定,却不会撤销那次求值的副作用。
两类读取操作保留着这种区别。NGINX 1.31.2, src/http/ngx_http_script.c, capture copy opcodes, lines 1343–1413 中的位置捕获长度与复制操作码,只检查 n < r->ncaptures;这只能证明偏移对索引落在已发布计数内,不能证明可选分组实际参与了匹配。未设置的 -1, -1 偏移对表现为零长度,而非一个匹配切片。因此,状态跟踪必须从最后一次相关迁移开始:映射正则返回 NGX_DECLINED 时不会发布成功匹配;改写正则封装器则会在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_regex_start_code(), lines 1065–1075 明确清除 r->ncaptures,却不会清除命名索引槽。可缓存性只决定取值函数是否会再次运行,并不会让命名或位置捕获状态变成不可变。
这条调用链还告诉调查者应在何处记录负面证据。一个没有消费者的已声明映射,在 ngx_http_map_variable() 之前就停止;映射虽被读取,但其来源无法取到候选值,则在相关查找分支之前停止;哈希命中在 ngx_http_regex_exec() 之前停止;正则未匹配,不会发布成功捕获;正则虽成功,但其捕获字节没有进入更早片段,则在公开依赖关系处停止。记录最早被切断的边,能够让另一位操作人员复现结论;只记录“存在映射”或“请求返回 200”做不到这一点。
对于非空的规范化键,组合哈希的优先级高于映射正则循环。存在漏洞的 NGINX 1.31.2, src/core/ngx_hash.c, ngx_hash_find_combined(), lines 211–245 依次尝试精确哈希、头部通配符和尾部通配符,并返回第一个结果;只有三者全部未命中,才会进入 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2530–2585 的正则遍历。规范化键为空时,ngx_http_map_find() 仍会调用组合哈希:辅助函数尝试精确哈希后,便在进入任一通配符阶段之前返回;正则循环的 len 条件为假,也会跳过。因此,“哈希未命中”只有在键非空时才表示三套组合哈希结构均未命中。优先级证据必须保留运行时实际使用的规范化键。
主机名规范化与正则大小写处理作用于不同表示。NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 107–154 会在主机名模式中先去掉一个末尾句点。输入非空时,查找函数为组合哈希键分配并生成小写副本;输入为空时,low 被设为空指针。若执行抵达正则,它在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2537–2568 接收的是已经过主机名规范化、但其余部分仍保持原样且未经小写转换的字节。解析器在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 527–562 决定按区分大小写的 ~ 或不区分大小写的 ~* 编译。通过改变大小写或末尾句点构造对照时,必须先计入这些转换,不能径直声称测试到了独立分支。
映射块的处理函数接线会在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 271–277 把条目交给行解析器,包含文件的分派则发生在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 417–419。NGINX 1.31.2, src/core/ngx_conf_file.c, ngx_conf_include(), lines 821–880 会展开平台模式,并在原位置解析文件;正则行则在遇到时经 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 534–559 追加。复现时必须采用目标平台的通配展开结果并保留内联顺序,绝不能对所有文件或配置行做全局重排。
映射会在处理匹配器之前去重结果表达式。存在漏洞的 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 421–510 先哈希并比较结果对象,继而复用或编译它;ngx_http_map(), lines 512–572 随后仍会区分匹配路径,把 var 赋给正则行,或传给 ngx_hash_add_key()。哈希构建器会在 NGINX 1.31.2, src/core/ngx_hash.c, ngx_hash_add_key(), lines 837–851 与 lines 1000–1010 保存这个值指针。运行时,ngx_http_map_find(), lines 2563–2571 按顺序执行正则行并返回胜出行存储的值,ngx_http_regex_exec(), lines 2699–2737 同时发布该行的捕获。即使结果指针相同,也不会合并匹配器身份或捕获副作用。
省略 default 时,映射使用共享的空变量值。解析完成后,存在漏洞的 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 286–293 会在没有显式默认值时选择 ngx_http_variable_null_value。NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_map_find(), lines 2539–2585 会在小写缓冲区分配失败、正则执行出错或所有匹配耗尽时返回空指针;取值函数再于 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 128–139 为所有空指针结果替换默认值。因此,默认值哨兵只能识别回退字节,无法说明查找为何返回空指针。只有排除分配与正则错误并确认取值函数成功完成后,才能记录“没有正则匹配”。
映射取值函数会在计算来源之前写入 http map started,却只在成功完成时写入来源和结果;这些位置见 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_variable(), lines 107–154。因此,一条开始记录只证明发生过一次取值尝试,并不证明成功。有效调试分支及其 HTTP 位掩码条件位于 NGINX 1.31.3, src/core/ngx_log.h, debug branches and macros, lines 84–145,无可变参数版本的 debug0/debug2 条件位于 no-variadic debug0/debug2 gates, lines 171–183;在 !NGX_DEBUG 下,本路径使用的宏会于 non-debug macros, lines 216–220 展开为空。只有构建来源已经证明支持调试,且请求的有效日志掩码启用了 NGX_LOG_DEBUG_HTTP,这些记录才可用于计数;否则,沉默并不能说明求值次数。两项前提均已成立时,来源与结果记录可以区分成功完成;若路由保持不变,重复的开始记录也可支持发生过多次尝试。
3.2 命名状态与位置状态读取方式不同,却由同一次匹配使 L 与 W 分叉
位置捕获走专门路径。存在漏洞的 ngx_http_script_compile() 在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_compile(), lines 480–498 识别 $1 至 $9,生成器保存加倍后的偏移对索引。存在漏洞的长度操作根据当前 r->captures[n] 与 r->captures[n+1] 计算长度;值操作稍后重新载入这些偏移和当前 r->captures_data。命名捕获则在 ngx_http_regex_exec() 覆盖其索引槽以后,由普通变量操作读取。表示不同,危险相同:任一种读取操作都没有绑定到其配对操作先前看见的状态。
正因如此,厂商临时缓解措施建议优先使用命名捕获,但不能将其简化为“命名捕获不会参与漏洞”。公开的缓冲区保护提交 b7675404… 正是用命名分组、显式为空的捕获,以及“捕获在前、映射在后”的表达式展示副作用。命名有助于提高局部性——操作人员可以把捕获引用限制在定义它的正则附近——却不会创造词法所有权或不可变快照。跨越定义它的关系重用命名捕获,仍然是在读取可变的请求状态。
令外层表达式为 C + S + M,其中 C 是捕获,S 是字面量文本,M 是映射结果。所有长度都指编码后的字节数,而非字符数。令 a 为惰性求值前的捕获长度,b 为映射正则执行后的捕获长度,s = |S|,m1 为长度程序看见的映射结果长度,m2 为值程序复制的映射结果长度,则 L = a + s + m1,W = b + s + m2,且 W-L = (b-a) + (m2-m1)。普通可缓存映射会缓存首次结果,所以 m1 = m2 = m;公式化简为 L = a+s+m、W = b+s+m,超出字节恰为 b-a。
下面的字节例子只说明方向,并不声称复现崩溃。若旧捕获为 2 字节、分隔符为 1 字节、缓存后的映射结果为 5 字节,存在漏洞的 1.31.2 会分配 L=8;若映射正则把捕获改成 11 字节,值程序便尝试写出 W=17,超出 9 字节。若 b=a,即使内容不同,这个公式也不会产生溢出;若 b<a,越界写入方向不存在,但所报告的结果长度可能覆盖尚未写入的尾部字节。修复版 ngx_http_complex_value(), lines 58–110 以 e.pos - e.buf.data 收口结果,而修复版 ngx_http_script_run(), lines 623–678 通过 e.pos + len - value->data 保留调用者预留的后缀。这是各自封装器的实际长度契约,不能互换成同一套公式。URI 转义同样要求比较编码后的字节,因为不同切片既会改变原始字节数,也会改变转义扩张量。
完整生命周期因此是:配置加载时注册分组元数据;请求到来时可能没有捕获,可能带有前一次正则留下的捕获,也可能已显式给命名变量赋值;长度操作读取此刻状态;第一次实际读取靠后的映射时,获胜正则发布新匹配;值程序从头运行并读取新状态;更晚的正则还可能再次替换它。每一个瞬间的状态本身都合法。错误在于把某一瞬间的有效观察当成了另一瞬间仍然成立的容量证明。

把映射放到受影响捕获之前,会改变这条普通顺序:长度程序先执行映射,再测量新发布的捕获;复制程序通常复用已经缓存的映射与同一捕获。这样可以消除特定的“捕获在前、映射在后”路径,却不是通用安全定理,因为其他副作用或不可缓存值的重新计算仍可能介入。修复版 1.31.3 改由有边界的调用者落实一般规则。ngx_http_complex_value(), lines 58–110 与 ngx_http_script_run(), lines 623–678 建立目标边界,并把失败状态传播给各自调用者;lines 1834–1884 中的嵌套改写路径建立嵌套边界并按实际长度收口。写入操作发现空间不足时,ngx_http_script_check_length(), lines 827–842 把执行转向退出操作码并设置错误状态,指令轨停止后,ngx_http_rewrite_handler(), lines 137–183 返回该状态。捕获写入操作本身只会在复制前于 NGINX 1.31.3, ngx_http_script_copy_capture_code(), lines 1439–1489 检查当前待写长度。捕获状态仍可改变,但它不再有权让复制越过分配边界。
三段公式可以直接扩展到含多个动态片段的真实表达式。令长度程序观察到各片段长度 x1…xn,值程序实际发出 y1…yn,则 L=Σxi、W=Σyi,发生溢出需要 Σ(yi-xi)>0。增长的捕获可能被随后缩短的变量部分或全部抵消,两个单独看来不大的增长也可能叠加。除非其余片段也都稳定,写入者不能仅凭某一片段局部相等就信任总量。这解释了为何只测试映射结果或只测试捕获长度,可能漏掉真正总和,也解释了修复代码为何让每次待写操作都针对同一个目标缓冲区末端检查。
两次观察都必须按字节表示分析。NGINX 的长度操作会计算配对复制操作准备发出的表示,包括选用 URI 转义操作时的扩张。一个捕获即使仍由 5 个 Unicode 字符组成,另一次匹配后也可能占据不同字节数;原始字节数相同的切片,如果需要转义的字节变多,最终输出同样会增长。相关比较从来不是屏幕宽度或字符数量,而是长度操作给出的编码后计数,与复制操作实际推进的字节数。测试记录应保存原始输入、选中分支、捕获偏移、编码模式、可观测条件下的预测总量、受保护执行的实际结果,以及最终游标长度。
| 测量时状态 | 惰性映射后的状态 | 预测值 L | 尝试写入 W | 解释 |
|---|---|---|---|---|
a=2, s=1, m=5 | b=11, m=5 | 8 | 17 | 可缓存映射改写捕获并使其增长,产生 9 字节超出量 |
a=11, s=1, m=5 | b=2, m=5 | 17 | 8 | 该表达式没有越界写入方向,实际输出更短 |
a=2, s=1, m1=9 | b=7, m2=4 | 12 | 12 | 状态改变,但正负长度差彼此抵消 |
a=2, s=1, m1=4 | b=7, m2=10 | 7 | 18 | 捕获增长与不可缓存结果增长叠加;仍须证明对应执行器可达 |
抵消行是重要对照。第二次观察得到不同值,只能证明状态可变,不能自动证明溢出;捕获变长证明出现一个正项,却不能在另一片段缩短时保证总和为正。反过来,只含一个变量的不可缓存改写表达式可以完全没有捕获算术,只要 m2>m1 仍会低估长度。因此,扫描器应报告有序的生产者与消费者,修复版上的边界测试应计算完整编码输出。任何一方都不应仅凭“出现不一致”就自行标注内存破坏。
命名捕获变量被有意设为可变,并可由多个正则定义共享。NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_add_variable(), lines 424–499 会对变量名做不区分大小写的比较;若已有变量不可变,函数拒绝复用,否则返回现有可变定义。正则编译会请求 NGX_HTTP_VAR_CHANGEABLE,并在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_compile(), lines 2631–2667 解析所得索引。因此,两个正则若发布同名捕获,便可能写入同一个请求槽。名称不会把状态限制在某一个正则块内;符号表必须同时按拼写和索引标识捕获。
命名捕获与映射输出都由索引变量读取操作消费,但二者的生产者不同。修复版 NGINX 1.31.3, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 219–236 会注册可变的映射取值函数;正则初始化则在 NGINX 1.31.3, src/http/ngx_http_variables.c, ngx_http_regex_compile(), lines 2648–2664 取得命名槽。若名称经不区分大小写比较后仍不同,映射结果和捕获会拥有不同索引与缓存规则;若比较后相同,包括只有字母大小写不同的写法,则会在 NGINX 1.31.3, src/http/ngx_http_variables.c, ngx_http_add_variable(), lines 424–466 复用同一个可变定义,而 NGINX 1.31.3, src/http/ngx_http_variables.c, ngx_http_get_variable_index(), lines 558–614 返回同一索引。注册顺序可能替换共享取值函数,因此复核必须保留声明顺序、标志以及最终处理函数的归属。
捕获向量有自己的容量计算,与输出目标完全无关。配置阶段会在 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_compile(), lines 2600–2630 记录最大分组数,再于 NGINX 1.31.2, src/http/ngx_http_core_module.c, ngx_http_core_init_main_conf(), lines 3511–3512 将其换算成 PCRE 偏移向量大小 (ncaptures + 1) * 3。运行时,只有含捕获的正则才进入 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2683–2694 的分配或复用分支:向量不存在或标记为需要重分配时才分配,否则复用;不含捕获的正则向量长度为零。这片工作区始终独立于脚本目标容量。
状态跟踪必须锁定最后一次捕获状态迁移,而不能只看最后一次成功的正则。匹配成功时,NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2699–2739 会发布计数、向量与数据基址。NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_copy_capture_len_code()/ngx_http_script_copy_capture_code(), lines 1343–1413 中的捕获操作码要求加倍后的索引小于当前已发布计数,并读取当前偏移对与基址;向量存在并不能证明某个可选子组参与了匹配,其偏移仍可能未设置。PCRE 封装器把结果转入 NGINX 整数捕获数组的过程见 NGINX 1.31.2, src/core/ngx_regex.c, ngx_regex_exec(), lines 390–460。此外,改写正则返回未匹配时,会在 NGINX 1.31.2, src/http/ngx_http_script.c, ngx_http_script_regex_start_code(), lines 1065–1075 清除 r->ncaptures。重放账目必须按发生顺序记录每次清除、发布与读取。
请求状态具有分层所有权。修复版会在 NGINX 1.31.3, src/http/ngx_http_core_module.c, ngx_http_subrequest(), lines 2426–2438 以清零方式分配子请求对象;随后,每个子请求都在 NGINX 1.31.3, src/http/ngx_http_core_module.c, ngx_http_subrequest(), lines 2505–2513 共享父请求的索引变量表。普通子请求以独立且为空的位置捕获字段起步;只有克隆子请求会在 NGINX 1.31.3, src/http/ngx_http_core_module.c, ngx_http_subrequest(), lines 2559–2575 别名父请求的捕获计数、向量与数据。此后带捕获的正则若依照 NGINX 1.31.3, src/http/ngx_http_variables.c, ngx_http_regex_exec(), lines 2683–2694 分配新向量,才会解除这份克隆别名。这里引用修复发布版只为证明状态结构,不能把这项行为归因于本 CVE 的修复。
3.3 volatile 允许重新计算;触发矩阵把语法候选与可达路径分开
第二类公开路径使用另一种观察方式。存在漏洞的 1.31.2 在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map(), lines 380–409 处理 volatile,设置映射上下文的 no_cacheable;映射块完成时,再在 NGINX 1.31.2, src/http/modules/ngx_http_map_module.c, ngx_http_map_block(), lines 266–288 把 NGX_HTTP_VAR_NOCACHEABLE 转给映射变量。取值函数运行后,ngx_http_get_indexed_variable() 把标志传到请求变量槽;ngx_http_get_flushed_variable() 看到它后清除缓存有效性,并按 NGINX 1.31.2, src/http/ngx_http_variables.c, ngx_http_get_indexed_variable()/ngx_http_get_flushed_variable(), lines 647–685 重新计算。
执行器差异是决定性条件。普通 ngx_http_complex_value() 只在入口清理一次,并让两条指令轨都使用 flushed=1;绝不能把它描述成必然在两遍之间重算 volatile 映射。公开的不可缓存示例落在改写模块的 set 路径。动态右值会让存在漏洞的 ngx_http_rewrite_value() 在 NGINX 1.31.2, src/http/modules/ngx_http_rewrite_module.c, ngx_http_rewrite_value(), lines 997–1017 发出嵌套复杂值记录和内嵌值操作;ngx_http_rewrite_set(), lines 934–959 则另行追加消费完整栈值的赋值记录。清零后的嵌套长度引擎与清零后的外层引擎都令 flushed=0,所以 NGINX 1.31.2, src/http/ngx_http_script.c, variable compilation and copy opcodes, lines 888–987 中的变量长度与复制分支会采用刷新式取值。已消费的不可缓存映射因而可以让长度程序得到 m1,值程序得到 m2;对单变量表达式,超出量就是 m2-m1。
| 场景 | 静态扫描特征 | 仍需证明的运行时事实 | 长度与复制结论 | 在修复构建上的安全确认 |
|---|---|---|---|---|
| 惰性“捕获在前、映射在后” | 正则映射可以设置命名或位置捕获;同一表达式先读取捕获,后读取映射结果 | 映射未被提前缓存、正则分支获胜、客户端可影响的输入让映射后捕获增长 | m2=m1;当 b>a 时危险 | 在 1.31.3 对应的虚拟主机、location、阶段与分支上运行,记录受控失败或输出 |
| “映射在前、捕获在后”的对照 | 组件相同,顺序相反 | 之后没有其他副作用再次改变捕获 | 这种普通顺序测量映射求值后的状态,因此不再形成公开描述的特定序列 | 用等价输入与“捕获在前”样例对照 |
| 正则没有相关且实际参与匹配的子组 | ~ 或 ~* 获胜,却没有提供更长且已被前面片段读取的子组 | 发布新匹配可能让旧的位置捕获不再可用;必须由其他片段提供正的总长度差 | 该映射没有正的子组增长项,但仍可能产生负的位置捕获漂移 | 跟踪最终 r->ncaptures 和总计 W-L,不能假定捕获向量未变 |
| 精确、通配符或主机名哈希命中 | 组合哈希在正则遍历之前解析了规范化来源 | 测试来源必须确实选择这一条哈希分支 | 正则执行器未被调用,因此本次查找不会发布成功捕获 | 与能抵达目标正则的非空哈希未命中输入配对比较 |
| 哈希未命中后的空来源 | 规范化来源长度为零,组合哈希也没有返回值 | 生产路径必须在来源求值和主机名规范化后仍保留空值 | 正则循环被跳过,处理函数选择默认值;本次查找不会发布成功捕获 | 比较空来源、非空哈希命中与非空正则命中三类对照 |
| 非空哈希未命中,所有正则均未匹配 | 来源抵达有序正则列表,但没有一项成功 | 必须证明默认值被选中前的每一条更早正则都未匹配 | 本次查找没有成功匹配状态发布;这不证明此前不存在捕获状态 | 记录每条尝试过的配置行,并与第一条正则成功的输入比较 |
| 普通映射已经缓存 | 同一请求树中任一更早消费者都可能填充共享索引槽 | 这次求值必须先于并支配通往写入点的每条路线,包括 error_page、命名位置、普通子请求与克隆子请求分支 | 已证明的路线可以复用缓存结果;另一条路线仍可能在候选表达式内首次求值 | 改变路由、请求树顺序和首次消费者位置;另一顶层请求不会预热本缓存 |
volatile 改写重算 | volatile 映射进入两份引擎都使用刷新式取值函数的改写复杂值路径 | 第一次求值改变输入或状态,第二次编码后结果更长 | m2>m1 或总计 W>L 时危险 | 在修复代码上驱动两次求值,检查边界保护与应用行为 |
| 状态改变但没有增长 | 存在危险顺序或重算,测试样例的第二次表示却等长或更短 | 其他攻击者可影响的输入仍可能增长 | 该样例不溢出,但不能构成普遍否定 | 测试边界类别和编码后长度,而非只测一条标称请求 |
| 没有不可信 HTTP 路径 | 候选关系存在,但来源或写入点不接收不可信请求数据 | 公开远程场景仍需一条进入正则和写入点的数据路径 | 仅是配置候选,尚未证明未认证可达性 | 记录变量来源与信任边界 |
stream 类比 | stream 映射与会话脚本的并行实现显示同一状态关系 | 协议特定可达性与影响需要另行证明 | 源码契约相似,但不扩大公开的恶意构造 HTTP 请求结论 | 纳入另行标记的 stream 加固审计 |
这张矩阵的输入应是展开所有包含文件后的有效配置,而不是只对主配置文件运行文本搜索。nginx -T 可能暴露凭据和内部路由,必须在受限条件下保管。应分别枚举 HTTP 映射与 stream 映射、来源表达式、精确项、字符串、掩码、默认项和正则项的优先级、所有 ~/~* 项、命名与位置分组、volatile,以及映射输出和受影响捕获的每个消费者。还要保留每个复杂值内部的组件顺序,再把它连接到虚拟主机、location、改写阶段、构建上游请求的指令、错误路径与客户端可控制的字段。静态输出只能是“不是候选”“需要路径分析的候选”或“已在修复代码上安全确认可达漂移”,绝不能直接写成“已利用”。
修复构建上的测试既要尝试触发假设,也要能证伪假设。保持制品、配置、虚拟主机、位置与写入点不变,每次只改变一个因果维度:哈希命中对照检验是否真的需要正则执行;颠倒组件顺序检验普通“捕获在前、映射在后”的依赖;保持编码后长度相等,检验只有状态变化而无增长时是否仍在容量内;移除 volatile,则在其他执行条件不变时专门检验重新计算。若候选与对照行为完全相同,应先检查分支选择和首次消费历史,再否定源码模型;若只有候选抵达边界保护,观察结果支持预测与复制发生不一致,却仍不能证明恶意意图或历史利用。
4 修复把长度预测变成可强制执行的写入边界
7 月 15 日发布的修复没有尝试让 map 提前求值、冻结每一组捕获,或禁止不可缓存变量。任何一种做法都会改变已经稳定多年的配置语言,而且仍不能排除其他有状态变量令两遍执行者得到不同结果。NGINX 改的是接收端契约:凡根据第一遍结果分配缓冲区的调用者,也必须向第二遍提供目标区间末端(该地址本身不可写);标准复制操作逐次检查这一边界,尺寸不一致时形成受控求值失败,而不是写出分配区间。最终外部结果由调用者决定:重写和直接请求构造器通常进入内部错误路径,标准封装返回 NGX_ERROR 或 NULL,而受守卫的访问日志格式操作失败时,处理器会放弃本次调用,却不会改写已完成响应。这也解释了补丁为何从共享脚本引擎继续传播到每类消费者。
这个契约有两个互补方向。若第二遍比预测更长,新加入的末端检查会在越界前停止;若第二遍更短,配套修改只报告实际生成的字节数,不再沿用第一遍预测,否则报告的结果长度可能把尚未写入的尾部字节也算在内。前者是 CVE-2026-42533 的核心内存安全修复,后者让同一套两遍接口在反向漂移时也如实描述结果。二者必须合并理解:多分配少量空间、静默截断或把余量清零,都不能恢复“测量结果与写入结果一致”的完整契约。
4.1 e.end 把目标区间纳入脚本引擎状态
在已修复的主线 1.31.3 中,src/http/ngx_http_script.h 的 ngx_http_script_engine_t,第 17–37 行在 pos 后加入 u_char *end。易受攻击的 1.31.2 在同一路径下只有当前指令位置和写入游标,没有目标区间。这个结构差异虽小却决定了边界由谁负责:复制操作码本来就是通用的,同一组字节码可以拼装重写结果、上游参数、文件候选名或另一段复杂值。把限制随引擎传递后,操作码无须知道是哪条指令请求字符串,也能在真正写入的位置执行内存安全检查。
已修复的 release-1.31.3/src/http/ngx_http_script.c,ngx_http_complex_value(),第 58–110 行保留原有长度循环和精确的 ngx_pnalloc(),在执行值字节码前设置 e.end = value->data + len,循环后检查 e.status,失败时返回 NGX_ERROR。通用执行器 ngx_http_script_run(),第 623–678 行把调用者预留的 len 与脚本预测合计为 n,设置相同末端;复制操作记录错误后返回 NULL。这些标准调用点正是把一个可选结构字段变成实际保护的地方。
核心守卫明示在 ngx_http_script_check_length(),第 827–842 行。若 end 为 NULL,函数出于源代码/API 兼容返回成功;否则它以有符号区间计算末端到当前游标的剩余空间,再与待写入的 size_t 长度比较。空间不足会生成含 no buffer space in script copy 的告警,把指令指针改到 ngx_http_script_exit,在引擎中设置 NGX_HTTP_INTERNAL_SERVER_ERROR,并返回 NGX_ERROR。因此调用者必须同时完成两件事:给出真实边界,并在状态非零后停止使用未完成值。
end == NULL 时继续工作的选择只表达 API/源代码兼容语义,不能被称为 ABI 自动安全保证。上游标准路径已经改为设置 end;自行构造 ngx_http_script_engine_t、只靠清零而不填写该字段的第三方模块仍获得旧行为。下游验收应搜索所有直接构造点,确定实际分配区间,设置 end,并传播 status。新头文件能够编译通过,不等于树外执行器的写入已经落在受保护边界内。
失败路径刻意没有采用静默截断。被截短的上游地址、授权相关变量、缓存键、文件名或协议参数,可能与预期完整字符串具有完全不同的含义;继续使用前缀可能把堆破坏换成请求走私、路由混淆或访问控制偏差。修复后的引擎终止求值,普通请求构造调用者把结果转入自身错误路径。具体消费者与阶段不同,受影响请求常表现为内部错误,但真正稳定且更强的约束是:从 end 地址起不写入任何字节,而且调用者明确知道值未构造成功。
稳定分支 1.30.4 实现的是同一设计,而不是分支特有绕行。其主要发布历史提交是 7ac67898bdfed2140a1bedcda252074cca11f494,主线对应 b767540492e8c79a58bc26034d3bab2f708b7bd1,二者共享补丁 ID a077d24f756826e1c55badd59c0590d29de6beea。审查厂商回移时,补丁身份比要求相同提交哈希更合理,因为不同历史不可能拥有相同父节点。验收问题应是引擎是否携带区间、相关调用者是否填写它、每次复制是否检查、失败是否向调用者传播,而不是软件包里是否碰巧出现主线对象名。
4.2 字面量、变量、捕获、转义与嵌套值复制共同服从一条守卫
最简单的修复操作位于 ngx_http_script_copy_code(),第 903–927 行。复制长度为 code->len 的已编译字面量前,它调用 ngx_http_script_check_length(),失败就立即返回。字面量通常不会在两遍之间变化,但仍纳入守卫能覆盖周围游标或预算已经漂移的调用者,也避免建立一套脆弱例外:安全性不该依赖维护者记住哪种操作码“理应不可变”。
动态变量必须在知道当次真实长度时检查。ngx_http_script_copy_var_len_code() 与 ngx_http_script_copy_var_code(),第 975–1034 行中,长度操作码返回测量阶段看到的变量值,值操作码再次取得槽位,并在 ngx_copy() 前用当前 value->len 检查空间。这个落点覆盖公开提交所述的不可缓存情形:修复不假定已刷新的变量会重现先前长度,也不需要为 map 单设黑名单。
捕获还要考虑参数转义可能扩张输出。已修复的 ngx_http_script_copy_capture_len_code() 与 ngx_http_script_copy_capture_code(),第 1401–1489 行根据请求当前捕获数组重新计算 len = end_offset - start_offset,源地址设为 captures_data + start_offset。原样复制检查 len;作为参数转义时,先用 ngx_escape_uri() 计算新增字节,再检查 len + escape。因此无论捕获本身增长,还是其转义表示增长,都不能穿过分配区间。
嵌套脚本上下文也必须获得当前对象的末端。正则重写启动时把 end 指向已分配重写缓冲区末端,嵌套复杂值执行时设为 buf.data + len。当 ngx_get_full_name() 用新分配的完整路径替换缓冲区,修复代码会把 pos 与 end 一起对齐到新值末端。边界属于当前被写对象,而不是引擎结构首次清零时碰巧存在的旧对象;缓冲区替换后继续携带旧末端,本身就会产生另一种对象/区间混淆。
正则替换与追加参数还拥有调用者管理的区域。修复版正则运行路径第 1192–1325 行建立重写结果末端,并检查普通变量或捕获操作之外追加的字节;完整名称路径第 1538–1561 行在所有权变化时同步重对齐游标与区间。由此可见,只给捕获复制加一条守卫仍不完整:字节码操作、手写分隔符和缓冲区替换都必须指向同一个当前对象。
提交 a8289aa69c74f7e664ad63b91c17aa2a554f190f 封闭了“实际结果缩短”方向。在 ngx_http_complex_value() 第 106–107 行,1.31.3 以 e.pos - e.buf.data 返回实际长度,不再返回预测的 e.buf.len;ngx_http_script_complex_value_code() 及新增末端操作码在第 1845–1884 行为嵌套值执行同类收口。这项修改本身不能阻止越界;它封闭的是另一个缩短情形:后续捕获状态令结果变短时,报告的结果长度可能把尚未写入的尾部字节也算在内。
ngx_http_script_run() 的实际长度公式不能简化成所有接口都只取“游标减起点”。它的 len 是调用者在脚本输出之外预留的字节,修复版本在第 623–678 行先令 n = len 再叠加脚本预测,最后写回 value->len = e.pos + len - value->data。这既保留调用者预留量,又用第二遍实际游标替代脚本部分旧预测。对于重写的 set 与其他嵌套复杂值,修复版 NGINX 1.31.3 在 src/http/modules/ngx_http_rewrite_module.c 的 ngx_http_rewrite_value(),第 1020–1027 行追加 ngx_http_script_complex_value_end_code,src/http/ngx_http_script.c 的嵌套运行时及末端操作码,第 1834–1884 行再用实际游标设置栈值长度。审查回移时若一律寻找 e.pos - value->data,反而可能删掉上层协议所需空间。
两个方向可形成一张验收真值表:预测等于输出时,游标抵达预期位置,行为不变;输出较短时,返回长度随真实游标收缩,并在 script_run() 中保留调用者预留的 len;输出较长时,下一次将越过 end 的操作记录失败并停止;分配失败时,调用者在值执行前沿既有错误路径返回。这比只测试一个特制映射更有约束力,因为它描述了未来每种变量实现都必须满足的接口性质。

end 处停止;后续补丁把同一限制带入日志,以及不经标准封装而直接执行脚本字节码的模块。4.3 HTTP 与 stream 共享加固,但公开攻击陈述仍以 HTTP 为界
stream 引擎获得同样的结构修复。在修复版 1.31.3 中,src/stream/ngx_stream_script.h 的 ngx_stream_script_engine_t,第 17–31 行加入 end 与引擎状态字段;ngx_stream_complex_value() 第 59–110 行设置限制并传播失败;ngx_stream_script_check_length() 第 690–705 行实施检查,字面量、变量和捕获复制再调用 stream 专用守卫。
stream 补丁还可逐操作核对:修复版字面量复制位于 src/stream/ngx_stream_script.c 第 766–790 行,变量测长/复制位于第 840–899 行,捕获测长/复制位于第 940–1001 行;完整名称解析更换缓冲区后,第 1049–1074 行传播失败并重对齐游标与末端。结构里出现 end,既不能证明每种复制都调用辅助函数,也不能证明更换缓冲区后区间仍正确,这些锚点正供稳定分支或发行版逐项验收。
这种对称性是共享引擎契约的源码证据,不是改写 CNA 范围的许可。F5 与 CVE 记录描述的是特制 HTTP 请求和数据平面 NGINX 工作进程,因此本文把 HTTP 作为公开材料已经建立的攻击路径。stream 维护者仍应接收上游加固并审查配置,因为代码所有者修复了同一假设;但仅凭对称差异,不能断言所有 stream 部署都满足该 CVE 公开的可达性和影响条件。
因为 end == NULL 仍被接受,树外模块必须单独列入清单。搜索源码与构建产物中直接实例化 ngx_http_script_engine_t 和 ngx_stream_script_engine_t 的位置,而不能只找 ngx_http_complex_value() 调用。逐处确定预测循环、目标分配、操作码周围的非脚本字节、缓冲区替换和错误返回。仅使用标准复杂值 API 的模块可继承守卫;直接运行值字节码的模块必须证明与下一章上游消费者相同的传播链。
失败传播也必须按接口书写。HTTP 共享辅助函数在引擎内部设置 NGX_HTTP_INTERNAL_SERVER_ERROR,但 ngx_http_complex_value() 对外暴露 NGX_ERROR,ngx_http_script_run() 暴露 NULL,最终请求路径由各自调用者选择;ngx_http_rewrite_module.c 的重写处理器,第 136–184 行直接返回引擎状态;协议/文件消费者走自身错误路径,stream 有自己的内部错误状态。访问日志中,第 5 章分别处理受守卫的格式操作失败、if= 过滤结果与动态文件名结果;三者都不会倒推改写已完成响应。因此“守卫总是返回 HTTP 500”抹去了调用者边界。
升级二进制后,应用可见行为变化不等于修复失败。此前两遍已悄然漂移的配置,可能开始返回内部错误并发出守卫告警。若漂移发生在受守卫的访问日志格式操作中,日志专用告警会伴随当前处理器本次调用被放弃,后续已配置目标也被跳过;第 5 章另行保留过滤条件与动态文件名分支。这是已修复引擎把潜伏状态依赖显性化。运营人员应把消息映射回具体表达式,删除危险捕获顺序或不必要的重新求值,再复测业务结果。压制告警或回退易受攻击二进制,既不会产生安全字符串,也不能恢复有效业务含义。
本章验收边界因而同时包含代码与行为:修复版 1.31.3 或稳定版 1.30.4 具备有界引擎;所有树内及相关树外直接执行者提供正确区间并观察状态;缩短结果报告实际长度;代表性流量得到预期字符串且不出现未解释守卫告警。达到这些条件后,才能在进入专用消费者审查前关闭通用脚本层。
稳定分支还提供第二套不可变基线。易受攻击的稳定提交 47c3628…/src/http/ngx_http_script.c 第 58–103 行精确分配后执行无边界第二遍;已修复稳定提交 017cf98…/src/http/ngx_http_script.c 第 58–109 行提供末端、传播失败并以实际输出收口,可选检查位于第 824–840 行。逐标签比较使软件包审查不必把某个主线提交哈希当作唯一证据,同时仍要求已发布代码具备相同不变量。
5 边界必须沿每一条执行复制字节码的路径传播
通用封装是中心,却不是所有入口。访问日志会编译自己的操作,先算行长,再把这些操作执行进日志缓冲区;若干上游协议与文件系统模块也直接执行脚本值,因为它们需要在生成字符串周围交错写入长度字段、分隔符、帧编码或末尾 NUL。修复前,这些路径继承相同的两遍假设,却不经过 ngx_http_complex_value()。完整回移因此必须顺着字节码找到每一个直接写入点,不能只看到共享结构中出现 e.end 就停止审查。
这也是把漏洞简称为“映射模块修复”为何会误导。map 仍是公开机制中的状态变更生产者,但真正的危险写入发生在任何把第一遍预算交给第二遍复制器的地方。上游修的是接收图:标准脚本、HTTP 与 stream 日志,以及八个直接 HTTP 消费者。这个图既为发行版与私有模块提供了可核对的等价性清单,也解释了为何公开修复由一组实质提交共同构成,而不是 ngx_http_map_module.c 里的一条条件判断。
5.1 访问日志有独立的测长/执行循环,也有独立失败消息
修复版 HTTP 访问日志操作类型改变了调用约定。在 release-1.31.3/src/http/modules/ngx_http_log_module.c 第 17–31 行,ngx_http_log_op_run_pt 同时接收 buf 与 end。它不是通用脚本引擎结构,而是日志模块自身的区间契约;常量片段、内置时间和状态字段、转义变量、JSON 变量及未转义变量,都通过操作签名获得明确末端。
处理函数说明了为什么这个末端不包括换行符。修复版 ngx_http_log_handler() 第 302–387 行把 NGX_LINEFEED_SIZE 加进分配总量,却把 end 定义在预留换行之前,再交给每一个操作;循环只在游标非 NULL 时继续。任何操作发现空间不足都会返回 NULL,处理函数随即返回 NGX_ERROR,而不是在失败复制之后仍追加换行。
缓冲与新分配/syslog 目的地虽用不同存储,却建立相同逻辑行末端:缓冲分支在第 341–354 行把可写区间止于预留换行之前,新分配/syslog 分支在第 375–388 行给新记录同一限制。这样可防止表面完整的回移只保护文件缓冲输出,却让 syslog 格式化器保留旧操作签名。测试应覆盖每种已配置目的地,尤其是同一请求写往多个目标的部署。
不同变量格式都按当次真实待写长度验算。普通路径第 1017–1072 行分别为缺失值横线检查 1 字节、为未转义值检查原长度,或为默认十六进制转义检查 value->len + escape_count * 3;JSON 路径在第 1136–1185 行检查 JSON 扩张;未转义操作和 ngx_http_log_check_length() 第 1206–1237 行输出另一条精确告警:no buffer space in log script copy。
这些分支不能共用一个预测数,因为转义取决于数据:原样值需要当前字节数;默认日志转义可把一个源字节换成三个十六进制字符;JSON 有另一套计数与表示;未找到变量则只写一个横线。修复后的操作计算自己即将写出的尺寸,调用日志专用辅助函数,区间不足时在写入前返回 NULL。它与核心引擎遵循同一“检查当前写入”原则,只是契约表达在日志操作调用约定中,而不是 ngx_http_script_engine_t 内。
stream 日志在 src/stream/ngx_stream_log_module.c 的 ngx_stream_log_handler(),第 249–335 行采用同样设计,普通、JSON 与未转义操作的守卫位于第 749–970 行。这份源码对称性支持回移完整性,却仍不扩大公开的 HTTP 攻击范围;它只证明上游在两个子系统里都识别出访问日志的两遍契约。
访问日志中有三种脚本上下文,失败接口并不相同。第一种是受守卫的日志格式操作。HTTP 的目标循环与两条操作执行分支见修复版 ngx_http_log_handler() 第 278–387 行:操作返回 NULL 后,处理函数立即返回 NGX_ERROR。日志专用辅助函数见ngx_http_log_check_length() 第 1206–1237 行,它在操作返回 NULL 前发出精确告警 no buffer space in log script copy。当前目标被放弃,同一次处理函数调用中排在后面的所有已配置目标也被跳过。stream 的目标循环与操作分支第 225–335 行和日志专用辅助函数第 940–970 行具有相同结果。这种源码对称性属于防御性 stream 覆盖,公开材料建立的特制请求可达性仍以 HTTP 为界。
第二种上下文是 access_log if= 过滤条件,它在该目标的格式操作之前求值。修复版 HTTP 分支在第 278–289 行调用标准 ngx_http_complex_value()。若这个 API 返回错误,处理函数就返回 NGX_ERROR,当前目标与后续目标均被跳过;若错误来自通用复制区间不足,核心辅助函数在第 827–842 行发出的告警是 no buffer space in script copy,其中没有 log。相反,过滤条件若成功求值为空值或 0,代码执行 continue:只跳过当前目标,后续已配置目标仍会运行。stream 分支在第 225–236 行通过 ngx_stream_complex_value() 实现同样的“API 错误/条件为假”分流;其通用辅助函数与告警位于第 690–705 行。
第三种上下文是动态日志文件名,它只在日志记录已经格式化后求值。HTTP 的 ngx_http_log_write() 第 430–452 行调用ngx_http_log_script_write() 第 484–552 行;后者运行 ngx_http_script_run(),并把 NULL 转成 len,即源码注释所说的“模拟日志写入成功”。当前目标因而没有写出记录,但 ngx_http_log_write() 把字节数当作成功,返回目标循环后继续处理后续已配置目标。若该文件名执行器因通用复制区间不足而返回 NULL,告警是 no buffer space in script copy,而不是日志格式操作专用告警。stream 在 ngx_stream_log_write() 第 366–401 行与 ngx_stream_log_script_write() 第 432–447 行实现对应的模拟成功路径。
外层调度器又形成一道边界。HTTP 外层 src/http/ngx_http_request.c 的日志阶段调度器,第 4015–4030 行和 stream 外层 src/stream/ngx_stream_handler.c 的调度器,第 314–329 行调用阶段处理函数时都不读取返回值。因此,格式操作返回 NULL 或过滤条件 API 报错,会停止本访问日志处理函数内的剩余目标,却不会倒推并把已经完成的 HTTP 响应改成 500,也不会以其他方式改写已经完成的 stream 会话;条件成功求值为假或动态文件名被模拟为成功,则只跳过当前目标。访问日志处理函数返回后,其他日志阶段处理函数仍可能继续运行。区分这三类缺失记录的形态,才能在保证内存有界的同时正确解释测试与取证结果;图 4 中的“压机丢弃”专指第一类日志格式操作失败。
5.2 八个直接消费者同时保护脚本字节和周围协议字节
提交 25f920eca977139dc6fc7638b419169a602a0064 覆盖 FastCGI、SCGI、uWSGI、HTTP/1 代理、HTTP/2 上游代理 proxy_v2、gRPC、索引与 try_files 的直接执行。这里的 proxy_v2 是 HTTP/2 上游代理实现,不能误写成 stream 模块或泛指代理协议。上述模块不能只在变量操作码周围设置 end:第一遍预算还包括脚本以外的 FastCGI/uWSGI 长度字段、SCGI NUL、HTTP 头字段冒号与 CRLF、HTTP/2 整数编码以及文件系统终止符。修复代码拆出每个脚本子区间,检查周围手写字节,必要时依据实际游标重算键/值长度,观察 e.status,并拒绝不一致的最终长度。
| 修复版 1.31.3 消费者与源码 | 建立的边界 | 额外字节或完成规则 | 失败结果 |
|---|---|---|---|
FastCGI,ngx_http_fastcgi_module.c 预算,第 877–922 行;执行,第 1072–1150 行 | e.end = b->last + params_len | 检查一字节或四字节编码长度,并要求 e.pos == e.end | NGX_ERROR;缩短时记录请求长度不一致 |
SCGI,ngx_http_scgi_module.c 预算,第 683–727 行;执行,第 819–914 行 | netstring 分配内的专用参数区间 | 检查每个值后的 NUL 与最终精确游标;外层逗号另行预留 | NGX_ERROR |
uWSGI,ngx_http_uwsgi_module.c 预算,第 880–937 行;执行,第 1041–1147 行 | 四字节 uWSGI 头之后的参数区间 | 检查键/值的双字节长度和最终精确游标 | NGX_ERROR |
HTTP/1 代理,ngx_http_proxy_module.c 预算,第 1240–1324 行;头字段,第 1413–1477 行;请求体,第 1518–1536 行 | 头字段与已配置请求体的独立区间 | 检查 ": "、CRLF、脚本状态与实际输出游标 | NGX_ERROR |
HTTP/2 上游代理 proxy_v2,ngx_http_proxy_v2_module.c 预算,第 522–534 行;头字段,第 730–817 行;请求体,第 965–998 行 | 临时键/值区间及头字段块与载荷末端 | 先取得实际临时长度,再进行整数和 HPACK 风格编码 | 脚本或帧空间不足时返回 NGX_ERROR |
gRPC,ngx_http_grpc_module.c 预算,第 760–873 行;执行,第 1063–1207 行 | 临时键/值区间及完整 HTTP/2 头字段块与帧载荷限制 | 在 HPACK 风格编码前使用实际长度,并验证载荷空间 | NGX_ERROR |
索引,ngx_http_index_module.c 第 123–208 行 | 已分配的映射候选路径 | 检查状态与尾部 NUL,再按实际游标取得 URI/路径长度 | NGX_HTTP_INTERNAL_SERVER_ERROR |
try_files,ngx_http_try_files_module.c 第 116–198 行 | 已分配的映射候选路径 | 检查状态与尾部 NUL;path.len 使用实际字节,而 alias 分支的 ngx_memmove() 仍使用预测 len | NGX_HTTP_INTERNAL_SERVER_ERROR |
FastCGI 最清楚地展示“双检查”。修复版 ngx_http_fastcgi_create_request() 把 params_len 从完整请求分配中单独算出,将 e.end 指向参数区末端,并在写入一字节或四字节编码长度前检查空间。每个键/值脚本必须在状态清零时完成,最终游标还必须等于预测末端。增长由边界阻断;缩短则由精确游标比较阻止形成外层长度仍旧、内容已缩短的畸形记录。
SCGI 与 uWSGI 把同一逻辑映射到各自线格式。SCGI 在生成值后需要一个 NUL;uWSGI 为键长度和值长度各写两个小端序字节。若游标已吞掉所在逻辑区间,只在 ngx_http_script_copy_var_code() 内守卫,并不能保护这些手写字节。直接消费者补丁因此在分隔符或长度字段前再调用 ngx_http_script_check_length(),并在脚本状态设置后停止。
HTTP/1 代理分别维护 headers_len 与 body_len。它约束头字段脚本,检查包围它们的冒号-空格对与 CRLF,再对独立生成的配置请求体设置另一边界。区间分离防止一个部分的漂移悄悄消耗另一个部分的预算,也说明“把末端指向整个物理分配最远端”为什么可能弱于上游:应受保护的对象是当前逻辑区域,不是大缓冲区能到达的最远地址。
gRPC 与 HTTP/2 上游代理 proxy_v2 还需第二层防护,因为生成的键/值先写进临时缓冲区,再编码进 HTTP/2 头字段块。修复代码分别以 tmp_len 约束临时脚本,从所得游标导出 key_len 或 val_len,之后把整数编码开销计入剩余头字段块容量检查。这样,第一遍错误键长度不会在临时复制已安全停止后,又通过编码函数逃出另一块内存边界。
索引与 try_files 是文件名消费者,不是上游协议。修复后的处理函数对候选区域内的字节码设限,观察引擎状态,检查终止 NUL 所需的一字节,再从 e.pos 得出实际路径/URI 长度。不过必须保留源码细节:ngx_http_try_files_handler() 第 164–198 行先把 path.len 更新为实际游标,alias 分支第 196 行的 ngx_memmove(name, name + alias, len - alias) 仍使用长度遍的预测 len。不能把补丁概括成“所有 alias 复制都已改用实际长度”;验收应按已发布代码描述,并把该分支纳入回归。
表格按家族压缩展示,但发布验收应保留每个模块的精确源码锚点。厂商构建可能禁用某个模块、携带改名分支,或增加私有直接执行器。应在交付源码或符号里寻找引擎构造与脚本值循环,再逐处证明区域边界、非脚本开销检查、状态传播与实际游标使用。上游八项是经验证的基线,不是所有商业发行版的封闭全集。

end 与守卫阻断增长,实际长度卡尺收口缩短结果;受守卫的日志格式操作压机在空间不足时丢弃当前处理器本次调用的余下目标,但外层调度器不会把已完成响应改成 500。四条管线表示四类协议/文件形态,并非与八个模块一一对应;断开的树外自定义执行器只有自己传入真实 end 并处理 status 才能获得保护。5.3 提交身份把本 CVE 修复与相邻的 7 月安全修改分开
发布历史序列从捕获复制的准备性简化 28219209e0b4f9e155fd8bd91ab81b8ac30628f2 开始。它只把 len 与源指针计算局部化,没有增加边界,不能单独被接受为修复。核心守卫是 b767540492e8c79a58bc26034d3bab2f708b7bd1;访问日志传播是 4d32a2703c79f92b7a99ce3547759cc599d6f83e;直接消费者是 25f920eca977139dc6fc7638b419169a602a0064;实际长度收口是 a8289aa69c74f7e664ad63b91c17aa2a554f190f。这些发布历史哈希与 PR #1561 中较短对象不同,因为公开发布分支重写了历史。
稳定版 1.30.4 因父历史不同而携带重写后的提交。对公开差异执行本地 git patch-id --stable 比较,得到下表五组身份。这个由仓库推导的计算可重复证明每项稳定版变换与对应主线变换相符;它不是厂商发布的校验和,也不能与带注释的发布标签对象混为一谈。
| 修复层 | 主线发布历史提交 | 稳定版重写 | 共同的稳定补丁 ID |
|---|---|---|---|
| 捕获复制的准备性简化;本身不是守卫 | 28219209e0b4… | ea47fabf55e2… | aa9b9769583442a32218cc76449c73bd618bcfe9 |
| HTTP 与 stream 通用引擎的末端、检查及状态契约 | b767540492e8… | 7ac67898bdfe… | a077d24f756826e1c55badd59c0590d29de6beea |
| HTTP 与 stream 访问日志操作边界 | 4d32a2703c79… | 78950bdcd443… | 30e9af247f43380f449daef421c4f05367eea137 |
| 八个 HTTP 直接消费端 | 25f920eca977… | 326b17b00383… | ffa90aeb9fd36b2cfad35877ea0d3354070a9cbf |
| 实际输出长度收口 | a8289aa69c74… | 97e40e59b7b4… | d722d619875fcd022c47a76d6e1fd8d417b507ea |
这张表支持精确的下游审查模型。先确定厂商采用哪一个上游基线,再逐项比较逻辑变换;因模块未编译而省略的文件,以及因私有模块直接运行引擎而新增的文件,都要计入。软件包变更日志即使提到相同 CVE,只要缺少这些变换就不足以通过;反过来,语义完整的厂商差异即使祖先历史中没有任何上游提交哈希,也可以被接受。
同一个 1.31.3 发布与 PR 还包含其他安全工作。发布历史提交 0cca8e055a2d909f1a00c2071665b502ec2fe94c 对应 PR 对象 96628b7,修复 slice 或后台缓存子请求触达的陈旧正则捕获状态,属于 CVE-2026-60005;SSI 子请求重复收尾的 PR 对象 1f03529 及其发布历史重写属于 CVE-2026-56434。两者都不能作为两遍分配漂移已经修复的证据。
拆分这些修改具有直接运营意义。下游厂商可以正确回移 CVE-2026-42533 而不把无关 SSI 补丁塞进同一提交,也可能只复制 SSI 补丁而没有修复脚本复制边界。若把 1.31.2 到 1.31.3 的整棵差异树归为一个 CVE,就无法审查等价性,还会制造错误安全感。台账应逐项写出字段不变量、影响路径、精确补丁或补丁 ID,以及每个问题预期通过的测试。
公开提交消息提供了代码形状之外的意图证据。核心修复明确列出两类配置:先前捕获被后续映射查找改写;以及不可缓存变量在长度与复制操作码之间改变长度。直接消费者消息明确点名代理、FastCGI、SCGI、uWSGI、gRPC、索引与 try_files,并说明附加的 proxy_v2 加固。消息支持修复意图与范围,固定到提交的修复版源码则证明最终发布的具体行。
因此一份可辩护的回移验收记录可以很紧凑,但不能空泛:记录易受攻击基线或厂商父提交;引擎末端/检查变换;访问日志操作末端变换;产品实际编译的每个直接消费端;实际长度收口;补丁身份或经独立审查的等价差异;以及针对性回归结果。只写“已修复 CVE-2026-42533”的软件包说明,不足以区分完整接收图与只遮住示例的症状补丁。
6 影响结论必须停在公开材料与源码证据能够支持的位置
机制证据建立了一个条件式事实:后一次值增长时,工作进程会在已分配堆对象之后写入;厂商材料则建立了所需配置存在时,远程提供的 HTTP 输入能够到达这条路径。这些事实足以要求紧急修复,却不能把所有理论后果压进同一标题。公开材料直接描述的结果是工作进程重启与服务损失;F5 进一步说明,ASLR 被禁用或能够绕过时,代码执行可能成立。每次提及更高影响都必须带上这个限定。反过来,ASLR 已开启或主进程能自动拉起工作进程,也不会消除越界写本身。
证据还有时间维度。CNA 评分、CISA ADP 判断、冻结版 KEV 缺席以及 NVD 信息扩充状态,分别在明确时间点回答不同问题。将来若出现利用报告,需要更新的是利用状态段落,而不是已经由源码固定的机制;将来若某发行版增加回移,需要更新的是该资产的修复答案,而不是上游受影响历史。把这些层次拆开,报告才能在新事实出现时准确更新,响应人员也能依据本地可达性而非过期标签做决策。
6.1 一条确定性请求路径即可破坏工作进程,并反复抽走服务容量
状态转换发生在单个请求内,且逻辑上是确定的:不需要第二线程与工作进程竞速,也不依赖释放后使用(use-after-free)时间窗。长度程序观察一个有效的捕获状态,之后的映射求值更新该请求的捕获状态,值程序再从表达式开头执行。若新捕获或不可缓存结果长于早先预测,旧复制代码就会越过内存池分配的边界。调度会改变周围堆布局和可见的崩溃方式,却不改变 W > L 这项逻辑不等式。
越界字节来自受请求影响的变量或捕获,因此比在边界触发普通断言更严重:工作进程可能先破坏相邻的内存池对象,之后才触发故障、记录日志或返回。源码审查可以确定目标区间、复制长度和缺失的检查;若没有在具名构建上进行受控复现,则不能确定恰好覆盖哪一个相邻对象、该对象字段之后如何使用,也不能证明攻击者选择的字节能在进程终止前存续至足以控制指令指针。
F5 描述工作进程重启,这与 NGINX 主进程/工作进程模型一致。工作进程处理数据平面连接,主进程负责配置控制与进程监督;单个工作进程异常退出并不自动证明控制平面访问、配置变化或主进程沦陷,CNA 也明确把问题标为数据平面。实际中断来自该工作进程上所有连接被切断,而替代工作进程会继续加载同一二进制与危险配置。
自动替换改变恢复时间,不改变漏洞。等价请求在新工作进程启动后再次抵达同一虚拟主机,状态关系就会重建。连续退出可能把流量集中到剩余工作进程、增加上游重试、耗尽连接队列并触发健康检查摘除;因此,即使一个工作进程在名义上只占一小部分容量,整个集群也可能失去远高于该比例的处理能力。
本地可用性估算要写出连接和依赖,而不是只抄 CVSS。记录每个实例的工作进程数、每个工作进程的连接数、客户端和上游的优雅重试行为、负载均衡器摘除阈值、备用副本,以及重连会冲击哪些身份服务或应用服务。承载长连接或昂贵上游请求的工作进程,会比承载短小无状态请求的工作进程造成更长的恢复尾部。CVSS 给出通用严重性,这份容量模型才决定变更窗口和批次。
ASLR 改变的是一个利用前提。F5 的措辞允许在 ASLR 禁用或可绕过时出现代码执行,故资产清单应记录加固与例外;但 ASLR 不阻止越界写入,不会扩大工作进程的堆对象,也不会阻断重复终止。以“ASLR 已启用”作为修复,会完整保留公开的 DoS 路径和内存破坏。
反向夸大同样无助。公开记录没有提供针对每个现代默认部署都可靠的任意代码执行链。要支持更高影响,至少需要一个确定且具名的易受攻击构建、编译器与分配器细节、确定的相邻对象、可控覆盖写、ASLR 绕过或禁用状态,以及进程死亡前可用的执行窗口。在这些证据被复现并与维护者协调前,本文保留 F5 的条件表达,不把“可能”升级为“已证实 RCE”。
CNA 的机密性与完整性分数表达评分模型下对易受攻击系统的潜在影响,不是已有数据失窃或配置篡改事件的公开通报。对具体事件,数据披露、命令执行或持久化需要独立证据,例如异常子进程、文件修改、可执行映射、网络目的地、凭据使用或内存工件。信号 11 与匹配映射配置可形成很强的内存安全假设,却不能自动证明所有下游后果。
这种校准不会降低优先级。互联网网关中由远程输入影响的堆覆盖写已有上游修复,并能重启工作进程;即使攻击复杂度高,它仍是高价值维护事项。精确限定会把响应指向真实门槛——二进制、配置数据流、流量控制、进程频繁变动与加固——而不是一条无法区分暴露与未暴露实例的笼统紧急消息。
6.2 major、9.2、KEV 缺席与 NVD 信息扩充回答的是不同问题
| 来源与冻结时间 | 公开状态 | 能够支持的结论 | 不能支持的结论 |
|---|---|---|---|
| nginx.org 安全公告 | major | 项目自身严重性词汇,以及受影响与修复分支陈述 | 数值 CVSS 或利用事件报告 |
| F5 CNA,2026 年 7 月 15 日发布、7 月 16 日更新 | CVSS 4.0 9.2 Critical;CVSS 3.1 8.1 High;CWE-122 | 厂商评分、网络/数据平面说明、条件、受影响产品与 ASLR 限定 | NIST 自行给出的分数,或默认环境可可靠 RCE 的证明 |
| 带时间戳的 CISA ADP 评估 | Exploitation: none;Automatable: no;Technical Impact: total | 信息扩充提供方在该时刻的判断 | 对未来利用工具或利用活动的永久预测 |
| CISA KEV 目录 v2026.07.16,7 月 20 日检查 | 该冻结目录 1,647 条中没有此 CVE | 该版本当时未把它列为“已知遭利用” | 利用不可能或从未发生的证据 |
| NVD,2026 年 7 月 20 日 | Awaiting Enrichment | NVD 已接收记录,但尚未发布 NIST 评估 | 把页面显示的 CNA 值称作“NVD 评分”的许可 |
nginx.org 标签与 F5 分数可以并存,因为它们属于不同体系。引用项目页面时把 major 换成“Critical”会误述原文;用 major 替代 CNA 的 9.2,又会丢掉数值向量。本文分别归因,让读者比较,资产政策再把任一来源映射到自身服务关键度,而不假装这些标签同义。
CVSS 4.0 向量记录网络可达、攻击复杂度高、无需权限和用户交互,对脆弱系统的机密性、完整性和可用性影响均高,且没有后续系统影响。3.1 向量用旧框架表达相近结果。配置和环境前提解释了攻击复杂度为何较高,却不会把输入变成经过认证的管理操作:部署满足网络与 TLS 前提后,请求无需应用凭据即可到达数据平面。
CISA ADP 的 Exploitation: none 是嵌在 CVE 记录中的带时间戳分类,书写时应使用过去时并注明提供方。Automatable: no 同样是当时对重复性与前提的判断,不保证未来的扫描器、配置指纹或利用测试工具无法降低操作成本。组织若已经掌握自身配置和客户端前提,仍然可以自动完成本地筛查与安全回归。
KEV 是 CISA 按其标准主动收录的正向目录,不是所有恶意请求的穷尽数据库。CVE 未出现在 v2026.07.16,只支持一条狭窄的历史陈述;它不能结束威胁狩猎、降低修复版本要求,也不能证明私下事件未发生。若以后版本收录该编号,冻结时点的陈述仍为真,但当前状态段必须更新。
NVD 的 “Awaiting Enrichment” 状态可防止归因漂移:页面可在 NIST 完成自身分析前展示 CNA 提供的 CVSS。来源登记应明确 9.2 与 8.1 来自 F5,这既满足区分厂商评估与 NIST 信息扩充的审计制度,也为将来 NVD 给出不同向量时保留修订空间。
发布日期、发现时间与修复时间也不同。CVE 于 5 月 5 日预留;发布与 CNA 记录在 7 月 15 日公开;记录和 ADP 材料于 7 月 16 日变化;F5 7 月 17 日更新公告;本文来源审查于 7 月 20 日冻结状态。披露前事件不会因为当时没有公开标识符而被排除,只是必须从进程、配置与请求证据调查,不能依靠同期告警标签匹配。
排序应把公开状态与本地字段结合:修复版或易受攻击的二进制文件、危险映射关系、互联网或伙伴网络可达性、客户端对源字符串的控制、工作进程崩溃历史、ASLR 状态、服务集中度与升级可用性。一个不在 KEV 的漏洞可以是暴露网关最高优先事项;离线镜像里没有合格配置的易受攻击包则可不同排期,但仍须在启用前升级。
冻结报告的状态句刻意有界:在所检查的披露窗口内,CISA ADP 记录为 Exploitation: none,且 CVE-2026-42533 不在 KEV v2026.07.16 中;条件式堆破坏仍应依据本地可达性与影响完成修复。它点明日期,没有把“未见”转成保证,因此以后出现新证据时仍能保持历史准确。
6.3 调查必须重新连起构建、表达式、状态变化、请求与进程退出
修复版告警 no buffer space in script copy 证明一个受守卫的通用脚本试图超过预测;受守卫的日志格式操作告警额外含 log。二者都不证明恶意意图或此前已有利用。生产请求、健康检查或罕见路由都可能首次触发存在多年的配置依赖。应把告警当作指向不一致表达式的高保真线索,保存请求上下文并修正关系,而不是直接给事件贴上利用标签。
易受攻击构建没有末端检查,因而不会发出新守卫告警。其证据可能很稀疏:异常工作进程退出、信号、核心转储、分配器诊断、PID 快速替换、连接重置、网关 5xx、上游重试或负载均衡健康状态变化。NGINX 错误日志可能给出信号与工作进程 PID,却没有生成字符串;访问日志既是受影响写入点之一,进程又可能在刷新前死亡,因此也可能不完整。
收集首先要冻结身份:记录工作进程映射的可执行文件路径与哈希、软件包或镜像摘要、nginx -V 输出、已加载的动态模块、主进程与工作进程 PID、进程启动时间及有效配置修订。进程启动后才更新的软件包数据库,不等于该进程已经映射了新对象。容器编排历史应在自动清理发生前保留上一实例和滚动发布修订。
以受限权限保存 nginx -T 生成的有效配置;其中可能含上游地址、路径、凭据和证书位置。枚举正则映射、命名与位置捕获、volatile 和每个消费端表达式。对候选虚拟主机写出精确求值顺序,判断客户端控制的 URI、头字段、参数、cookie 或派生变量能否进入映射源。
状态台账至少包含预测分段、改变状态的查找、查找后捕获偏移或变量长度、实际写入分段、写入点,以及守卫/崩溃结果。没有必要保存完整敏感值:长度、变量索引、表达式标识符、请求关联 ID 与安全哈希后的源,已经能证明 b > a,同时避免新建一套凭据或个人数据仓库。
时间对齐要跨多个时钟。统一边缘设备或负载均衡器的请求时间、NGINX 错误日志和访问日志、内核或服务管理器事件、容器重启、上游重试与外部可用性探针,并记录时区和时钟偏差。工作进程可能在接收请求与写出最终日志之间退出,所以最强关联有时是边缘设备看到一个请求,随后某个工作进程 PID 消失,且上游多处遥测出现相同的连接重置。
单独一个信号 11 并非本 CVE 专属。动态模块、其他请求造成的分配器破坏、硬件故障,以及 7 月其他安全问题都可能终止工作进程。二进制文件受影响、有效表达式具备公开顺序或不可缓存行为、输入可控、观察到长度漂移且崩溃在该路径复现时,CVE 假设增强;任何必要条件缺失,或修复构建在守卫处安全停止时,则应相应收窄。
代码执行归因的门槛更高。检查异常进程祖先关系、可执行内存或模块、文件写入、新账户、持久化、异常出站连接、凭据使用与控制平面变更。一次堆溢出崩溃不能建立这些后果;反之,若发现子进程或持久化,也不能因为公开材料强调的直接结果是重启就予以忽略,而应将其作为可能独立发生或已经升级的事件调查。
核心转储和内存检测器报告可能含完整请求、头字段、秘密、上游数据或其他连接的内存,必须限制收集、加密、保留和披露。每条调用栈旁都应记录编译器、优化选项、分配器、内存检测器、架构与精确提交。发行版回移会移动源码行号;只有函数、指令、对象区间与构建身份才能使发现可复现。
实用证据矩阵以每起疑似事件为一行,列出运行时身份、配置数据流、输入关联、首个异常操作、进程结果、服务影响与更高影响指标。未知项就写未知,不要强迫得出二元结论。如此便能让审查者看清“易受攻击且可达”“已守卫的不一致”“与 CVE 一致的崩溃”及“已证实的利用后果”之间的差别。
本文没有执行本地利用、工作进程崩溃复现、PoC 验证或生产遥测审查。机制结论来自不可变的上游源码与公开修复提交;影响和产品结论来自所引厂商、CNA 与政府记录。任何似乎能证明更高影响的新运行,应先作为私有材料保留并与维护者协调,不能未经验证就写成公开标题。
7 关闭项必须同时替换易受攻击代码、危险状态关系与全部旧进程代际
一次成功变更需要三份彼此独立的证明:运行工件包含所属分支或产品的完整修复;有效配置已经按公开顺序与不可缓存路径审查,测试中出现的守卫告警得到解决而非被隐藏;任何还能接收生产流量的工作进程或旧主进程都不再映射易受攻击可执行文件。安装软件包、通过语法检查或一个绿色探针,只能证明其中一部分。
工作从运行时身份开始,以流量证据结束;中间还包括配置来源、包含文件展开、下游产品映射、金丝雀行为、协议回归、进程控制语义、回滚与历史事件复核。把这些字段放进同一变更记录,例外才不会消失在集群百分比后面:等待固件的设备、没有末端指针的树外模块、迟迟未退出的旧工作进程,或罕见映射路由升级后触发的受控错误,都必须保留具名负责人与结论。
7.1 盘点进程实际映射的可执行文件,并把有效配置追成数据流
从每个嵌入 NGINX 的监听器与工作负载开始:上游 OSS 软件包、NGINX Plus、入口控制器或网关控制器、WAF 设备、边车、容器、虚拟机镜像、灾备节点与自动扩缩容模板。记录服务负责人、网络范围、角色、发布线、软件包或镜像来源、摘要、当前主进程与工作进程 PID、启动时间、动态模块、流量份额与回滚候选项。休眠模板同样重要,事故期间紧急扩容可能在在线集群已经修复后又启动旧镜像。
从实际可执行文件路径采集 nginx -v 与 nginx -V,并与文件哈希或镜像层关联。Server 响应头可以被隐藏、重写或由另一层代理发出,不能作为运行时证明;软件包管理器显示的版本也可能与更新前已经启动的进程不一致。在 Linux 上,可以用进程的可执行文件映射或 /proc 下的可执行文件链接把 PID 绑定到 inode;容器还要记录镜像摘要与工作负载修订。
发行版回移需要证据链,而不是字符串比较:厂商公告把软件包构建映射到 CVE;源码或补丁显示受守卫的通用引擎、日志操作、该构建实际包含的直接消费端与实际长度处理;交付仓库或镜像包含该软件包;部署选择了它;运行中的进程再映射到它。每缺一环都会产生常见的假关闭:仓库已修但镜像仍旧,镜像已修但滚动发布未更新,软件包已安装但 PID 仍旧,或只有挂着 CVE 标签的不完整补丁。
用 nginx -T 获取包含文件展开后的有效配置,并按敏感信息保护输出。应先固定准确的二进制文件与模块集合,因为旧解析器或私有模块可能改变哪些指令会被接受。保存校验和、采集时间、主机或工作负载身份和配置修订,使审查者能区分当时测试的内容与之后部署的版本。
枚举每个 HTTP map 块,不要只检查文件名像路由的配置。逐项记录源复杂值、结果变量、字符串键、以 ~ 或 ~* 开头的条目、命名和位置捕获、复杂结果、volatile、包含文件来源与消费它的虚拟主机。映射声明本身是惰性的,只有某条指令读取结果时才进入实际求值图。
之后建立使用关系图,搜索 set、rewrite 和 return 表达式、代理 URL 与头字段、已配置的请求体、FastCGI/SCGI/uWSGI 参数、gRPC 头字段、索引、try_files 与日志格式,并保留表达式顺序。第一类公开机制要求捕获在同一生成字符串中先于会改写它的映射结果;颠倒顺序或拆分值,可能改变第一次测量是否会变成陈旧结果。
命名捕获与位置捕获即使源自同一正则,也要分开列项。命名捕获占用变量槽位,正则执行会更新其数据和长度;位置捕获使用请求级偏移对与 captures_data。其他正则指令同样可能更新这些请求字段。应追踪哪次匹配建立了映射前捕获、哪次映射匹配替换了它,不能只凭变量拼写推断所有权。
volatile 类问题需要精确到重写执行上下文,不能假定每个复杂值都会在两遍各重算一次。识别经 ngx_http_script_complex_value_code() 编译的 set 值、其中的不可缓存映射变量,以及从其长度求值到外层复制求值之间会变化的捕获或输入。普通 ngx_http_complex_value() 开始时只预清理一次,并在两遍都使用索引取值函数;所以把映射标成 volatile 只是候选条件,不是所有消费端都会双重求值的证明。
每个候选项都要标出远程数据来源。URI、参数、头字段、Cookie、Host、客户端地址、TLS 元数据、上游派生值与内部生成变量,拥有不同的控制程度和归一化过程。记录前置代理的变换、请求尺寸限制、认证或证书条件,以及调用者能否选出令后一次捕获更长的输入,才能把“受影响二进制文件加正则映射”转成可复现的可达性陈述。
实用工作表可以每个写入点为一行,列出映射源、正则、所生成捕获、初始捕获生成方、表达式顺序、不可缓存上下文、客户端可控字段、第一遍长度、可能的第二遍长度、修复版构建的行为与负责人。不能满足增长条件的行保留为有依据的未暴露结论;包含文件或模块行为不确定的行继续保持未关闭,不能直接向下判定为安全。
不要以禁用全部 map 作为响应。多数映射是普通字符串查找或安全的正则使用,全局删除会破坏路由、租户隔离、缓存与认证。目标是升级二进制文件,并消除审查发现的危险状态依赖。任何配置重写都应显式呈现求值关系,并在一组具有代表性的输入上保持预期业务结果。
树外模块也要进入同一工作表:记录源码修订、构建标志、引擎构造点、分配公式、末端指针设置、手写帧封装写入、状态处理与厂商支持。调用标准复杂值 API 的模块可能继承修复,直接字节码执行器则必须证明自己的受守卫区域。仅按新的 NGINX 头文件重新编译而不设置 end 不会产生保护,因为 NULL 仍是兼容路径。
盘点最后补齐容量与所有权。每个实例记录连接数、每秒请求数、冗余、排空时间、维护窗口,以及获准修改配置和二进制文件的负责人。这些字段决定滚动发布批次,避免技术上正确的安全修复造成可避免的可用性事件,也使事件响应能够迅速查到某个可疑请求由哪一代进程处理。
7.2 每个下游产品采用自己的修复工件,而 None 表示必须迁移
上游 OSS 当前稳定线 1.30.0–1.30.3 应转到 1.30.4 或更新的稳定版;当前主线应转到 1.31.3 或更新的主线版本。更早的 1.28、1.26 及其他历史分支处于 nginx.org 从 0.9.6 开始的受影响历史内,却没有在那些分支上命名修复点;它们应迁移到受支持的修复线,或使用能够独立证明等价回移的发行版软件包。
NGINX Plus 必须记录完整产品标识符。F5 将 Plus 37 LTS 37.0.0.1–37.0.2.1 列为受影响,修复版为 PLS.37.0.3.1 LTS;在此前的发布模式下,R33–R36 受影响,R36 P7 是修复候选版本,更旧且没有自身候选版本的发布需要迁移。只写“R37”或“Plus 已更新”,会丢失验收必需的补丁级别。
| F5/NGINX 产品 | F5 公布的受影响版本 | 修复候选版本或所需行动 | 验收证据 |
|---|---|---|---|
| F5 WAF for NGINX 5.x | 5.9.0–5.13.3 | 5.13.4 | 产品软件包/镜像,以及运行中嵌入式核心/模块身份 |
| NGINX Gateway Fabric 2.x | 2.0.0–2.6.6 | 2.6.7 | 控制器和数据平面镜像摘要,加滚动发布修订 |
| NGINX Ingress Controller 5.x | 5.0.0–5.5.2 | 5.5.3 | 控制器镜像、受管 NGINX 进程身份与生成配置 |
| NGINX Ingress Controller 5.x LTS | 2026-lts-r1–r3 | 2026-lts-r4 | 完整 LTS 修订与 pod 替换 |
| Instance Manager 2.17.0–2.22.1;App Protect WAF 4.11.0–4.16.0 与 5.2.0–5.8.0;NGINX Gateway Fabric 1.3.0–1.6.2;NGINX Ingress Controller 3.5.0–3.7.2 与 4.0.0–4.0.1 | 列为受影响 | 原产品线没有候选版本;迁移到含修复的受支持发布 | 新的产品线映射,而不是永久例外 |
下游控制器或 WAF 可以自行打包 NGINX 二进制文件、模块、生成配置与更新机制;更新宿主机上不相关的 nginx 软件包不会修改嵌入式进程。清单中应把“产品版本”与“实际运行的 NGINX/模块身份”分列,依据厂商产品目标与进程替换程序关闭,而不是把 1.31.3 机械套到每个带 NGINX 品牌的工件。
F5 表里的 None 不是“不受影响”,而是该受影响产品线没有列出更新候选版本,通用行动是迁移到含修复的版本。仍留在此类分支的例外必须有厂商工单、隔离与容量控制、负责人、迁移计划和到期日,不能因单元格没有版本号就标记为已修复。
F5 的“不受影响”清单也有范围:它评估尚未达到 End of Technical Support 的软件版本。在此范围内,公告列出 BIG-IP Next SPK、BIG-IP Next CNF、BIG-IP Next for Kubernetes、BIG-IP all modules、BIG-IQ Centralized Management、F5 Distributed Cloud all services、F5 Silverline all services、NGINX One Console、F5OS-A、F5OS-C、NGINX all other products、Traffix SDC 与 F5 AI Gateway 为不受影响;不能把这个结论外推到未评估的 EoTS 发布。
共享品牌名不是数据流。不应仅因产品带 NGINX 名称,就把“不受影响”清单上的产品加进受影响面;也不能仅因受支持的同系列产品已评估,就清除 EoTS 产品。证据单位是一个具名产品、版本、嵌入组件、配置和厂商陈述。这样既避免不必要的紧急变更,也避免毫无依据地放心。
F5 的临时配置建议是避免未命名捕获,使用命名捕获,并只在包含相应正则匹配的块内引用该命名捕获;应以厂商缓解建议的原意保留。源码推导审查还要求删除“捕获先于映射”的危险依赖,并消除不必要的不可缓存重新求值。两者都可能改变路由或变量生命周期,必须先在隔离副本上比较输出。
临时重写不等价于已修复写入点。新包含文件、未来的映射消费端、第三方模块或另一种会改变状态的变量,都可能再次引入两遍不一致。配置变化可以作为纵深防御和降低即时可达性的措施,二进制文件升级仍是完成标准。ASLR 与工作进程监督同样只能降低一个影响条件或缩短中断,不能强制执行目标区间。
厂商回移需要的证据不止 CVE 标签:可选引擎末端字段、通用守卫与失败状态、字面量/变量/捕获/转义检查、嵌套值与正则缓冲区的区间、访问日志操作的末端、该产品编译进的直接消费端,以及实际结果长度。历史允许时,与上游匹配的补丁 ID 是强证据;厂商重组过代码时,经独立审查且逻辑等价的干净差异也可接受。
第三方模块兼容需要构建与行为计划。按目标 NGINX 头文件重新构建,复核每个直接执行器,运行聚焦测试,并保留模块源码修订与工件哈希。若模块不能设置正确的区域末端或传播失败,应移除或隔离,而不是假设上游二进制文件会包住每次写入。可选字段是为了避免造成硬性的源码破坏而保留,这恰恰让显式模块审查更重要。
回滚工件必须已经具备安全不变量。因为无关部署检查失败而退回 1.31.2 或 1.30.3,会重新开放已知漏洞。应准备一个已经包含回移的前一版已知良好构建,或把应用与配置的回滚同二进制文件安全更新分离,明确哪些元素可以安全回退、哪些不能。
按故障域与容量安排产品升级。先从低流量成员或金丝雀成员开始,为重试负载保留足够的已修复或隔离容量,观察长连接和上游行为,再逐步扩大。控制器要确认调谐过程不会重建旧镜像或配置;自动扩缩容组要先更新启动模板,才可把在线节点替换计为完成。
7.3 金丝雀测试与进程代际证据共同证明修复正在服务流量
金丝雀验证从 nginx -t 开始,但它只证明语法和静态配置加载,不执行每一个惰性映射,不重现脚本的两遍执行,也不证明哪个工作进程映射的二进制文件会接收流量。随后必须用代表性请求覆盖每个候选映射源、正则分支、捕获长度、消费端写入点与生产环境实际启用的上游协议。
聚焦机制的测试语料需要安全的长度转换,而不是破坏性载荷。为每个候选表达式选择不匹配、等长捕获替换、较短替换与较长替换,只在已修复且隔离的构建或金丝雀构建上执行;观察响应状态、选中路由、上游头字段或参数、文件候选项、访问日志、两种守卫字符串、工作进程 PID 与错误速率。
预期结果不能简化成“全部返回 200”。此前不一致的表达式在修复版可能返回内部错误和 no buffer space in script copy;受守卫的访问日志格式操作发生不一致时,则会保留已完成响应,同时发出 no buffer space in log script copy,并放弃该访问日志处理器本次调用中后续已配置日志目标。二者都是安全停止,也都要求先修配置再扩大滚动发布;后者不会经外层调度器把已完成响应改成 500。if= 过滤条件与动态文件名分支按第 5 章所述具有不同的目标跳过行为。
重写危险表达式后,重放同一输入集并比较业务输出。核对 URI 或路由、缓存键、代理目的地、每个已配置的上游头字段和参数、响应体、索引或 try_files 结果与访问日志字段。仅靠删除变量来消除告警,可能改变授权判断或租户路由;回归判定基准必须是预期应用行为,而不是消息消失。
执行构建中存在的每个直接消费端。FastCGI、SCGI 与 uWSGI 应检查解码后的参数记录和长度;代理应检查头字段分隔符与已配置请求体;gRPC/HTTP/2 上游代理应解码生成的头字段与帧载荷长度;索引与 try_files 应验证实际路径选择和终止。这能发现通用引擎已安全、专用构造仍未检查的不完整回移。
除增长外还要测试缩短。实际长度这一配套修复会阻止返回值或嵌套值所报告的长度把尚未写入的尾部字节也算在内;输出长度须逐字节比较,包括参数转义与重写结果。只阻断越界写却仍返回第一遍长度的软件包,仍可能把尚未写入的字节计入值的报告长度,或生成畸形协议字段,进程存活并不是完整证据。
保留正向控制。普通字符串映射、捕获稳定的正则映射、有效的 volatile 重写、正常日志、上游参数生成、索引解析与 try_files 回退都应继续工作。禁用全部映射、外部模块或上游集成虽可能避开样例,却不等于产品通过;正向控制用来区分预期守卫与运营绕行。
进程替换取决于控制路径。HUP 重载是配置重载,不是二进制文件升级。官方配置重载流程和修复版 release-1.31.3/src/os/unix/ngx_process_cycle.c 主进程重配置分支,第 211–244 行都表明:当前主进程重读配置、启动新工作进程、要求旧工作进程优雅关闭;这些新工作进程仍由当前主进程的可执行文件派生。安装新文件后发送 HUP 并不会执行新二进制文件。
可用性模型允许时,服务管理器重启是直接的二进制替换路径。通过受管服务单元停止或重启服务,确认旧主进程与工作进程退出,再验证新主进程映射修复版可执行文件的 inode 或镜像;保留配置测试、重启时间戳、监听器恢复情况、客户端错误速率与回滚控制。重启可能中断连接,所以仍需批次、冗余与容量规划。
NGINX 官方热二进制升级流程不同:向旧主进程发送 USR2 后,它重命名 PID 文件并执行新二进制文件,建立新主进程与工作进程代际,旧代际起初仍可提供服务。修复版源码在 ngx_process_cycle.c 第 262–273 行处理更换二进制文件与停止接收连接的控制分支。随后向旧主进程发送 WINCH,优雅停止旧工作进程;等待并验证新代际;最后向旧主进程发送 QUIT。准确顺序是 USR2 → 向旧主进程发送 WINCH → 验证 → 向旧主进程发送 QUIT。
优雅退出可以观测,却不是瞬间完成。ngx_worker_process_cycle() 及其优雅关闭路径,第 698–749 行显示,正在退出的工作进程先关闭监听套接字与空闲连接,再等待未完成的工作和定时器。长连接或卡住的连接会使易受攻击工作进程在变更看似健康后仍映射旧可执行文件;必须记录旧 PID 并等待实际退出。
验收记录不能混用三种程序。HUP 证明配置代际改变,不证明可执行文件改变;受管重启以预期的中断语义替换进程;USR2 热升级暂时产生两个主进程,需要明确 WINCH、观测与 QUIT。三者的证据和回滚路径都不同,一个名为“已重载”的通用勾选项不能证明修复版二进制文件已经接管全部流量。
无论采用哪种程序,都要在前后采集主进程与工作进程 PID、父 PID、启动时间、可执行文件映射或镜像摘要、配置校验和、监听器所有权与退出时间,确认没有旧主进程或工作进程还能接收生产连接。外部探针应全程运行,本地指标应显示工作进程退出、重启、5xx、连接重置、时延与上游重试速率回归基线。
热升级时要刻意验证两代进程:排空旧代际前先确认新主进程使用修复版二进制文件与预期配置。若验证失败,在旧主进程仍可用时按官方流程回滚,不要删除证据,也不要无限期留下两代共同接收流量。成功后向旧主进程发送 QUIT,并核实其 PID 文件和进程都已消失。
容器或编排系统的滚动发布即使隐藏信号,也有对应的进程代际。确认工作负载模板引用已修复且不可变的镜像,每个旧 pod 或任务均已终止,就绪检查覆盖有意义的请求路径,负载均衡器不再向旧端点路由。部署标记为完成时,若正在终止的 pod 仍服务长连接,暴露仍可像优雅退出中的旧工作进程一样继续存在。
观测窗口要覆盖低频路由、缓存刷新、定时健康检查与正常流量变化。分别针对两条守卫消息、工作进程异常退出、PID 频繁变动、5xx、连接重置与时延发出告警。重启后安静一分钟无法覆盖每月才走一次的租户映射;负责人应根据真实使用和风险定义窗口。
| 验收层 | 所需证据 | 导致事项不能关闭的情况 |
|---|---|---|
| 工件 | 修复后的上游版本或完整等价回移、软件包或镜像摘要、模块清单 | 只有版本标识、不完整补丁、嵌入式核心未知 |
| 配置 | 有效的 nginx -T 配置图、捕获与映射顺序、volatile 重写上下文、远程源 | 只统计关键词,没有消费端或包含文件 |
| 行为 | 跨已编译写入点的增长、等长、缩短语料与正向控制 | 守卫告警未解释、输出改变、只做语法测试 |
| 进程 | 没有仍在接收流量的进程映射旧二进制文件;程序特定的 HUP、重启或 USR2 证据 | 只安装软件包或发送 HUP,旧可执行文件仍在 |
| 服务 | 外部健康、连接重置、5xx、时延、重试与容量回归基线 | 端口开放,但隧道、路由或上游操作仍然失败 |
| 事件 | 历史崩溃已关联或明确标为未解决;更高影响证据已经评估 | 以 KEV 缺席、ASLR 或单条缺失访问日志作为关闭依据 |

工程教训比“永远不要使用惰性变量”更窄,也比某一种映射临时绕行更持久:预测可以指导分配,只有当前目标区间才授权写入。NGINX 1.31.3 与 1.30.4 把这条规则写入标准复制,并传播给直接消费端。生产环境的验收闭环同样遵循它:预期版本或配置仍只是一项预测,直到运行时身份、实际进程代际、代表性输出与观测到的服务状态都留在已验证边界内。
研究记录
8证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
8.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
依赖特定配置关系才能触发的 NGINX 正则映射堆缓冲区溢出
上游历史受影响范围;仍须根据稳定版或主线版分支选择相应修复版本
稳定版与主线版的首个修复版本,依次为 1.30.4 和 1.31.3
F5 列出的 NGINX Plus 修复工件;没有修复候选版本的旧产品线必须迁移
已修复构建在脚本实际输出超过预测缓冲区时发出的守卫信号;应调查对应表达式,但不能仅凭这条消息认定已经遭到利用
已修复构建的访问日志守卫信号;当前日志处理函数的本次调用会停止,并可能跳过后续已配置的日志目标,但不会把已经完成的响应改写为 500
8.2事件时间
- 0.9.6 引入正则映射
NGINX 0.9.6 为 map 引入正则匹配;nginx.org 后来把该版本列为 CVE-2026-42533 历史受影响范围的下限。
- CVE 编号预留
CVE-2026-42533 编号被预留。
- 修复版本与公告公开
NGINX 1.30.4、1.31.3、NGINX Plus PLS.37.0.3.1 LTS 与 R36 P7 同日发布;官方安全修复 PR 也在当天合并,F5 CNA 同日公开该记录。
- CVE 记录加入 CISA ADP 评估
CVE 记录更新,加入 CISA ADP 带时间戳的评估:Exploitation: none、Automatable: no、Technical Impact: total。
- F5 更新安全公告
F5 更新安全公告;本文采用这版公告所列的受影响范围和修复候选矩阵。
- SOSEC 冻结证据基线
SOSEC 固定官方发布标签、修复提交、产品范围与公开利用状态,形成可复核的证据基线。
8.3来源与材料
- F5 K000162097 安全公告https://my.f5.com/manage/s/article/K000162097
- F5 CNA 发布的 CVE-2026-42533 原始 JSONhttps://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/42xxx/CVE-2026-42533.json
- nginx.org 安全公告索引https://nginx.org/en/security_advisories.html
- nginx.org 2026 年发布新闻https://nginx.org/2026.html
- nginx 1.31.3 主线版变更记录(CHANGES)https://nginx.org/en/CHANGES
- nginx 1.30.4 稳定版变更记录(CHANGES)https://nginx.org/en/CHANGES-1.30
- ngx_http_map_module 官方文档https://nginx.org/en/docs/http/ngx_http_map_module.html
- NGINX 官方 PR #1561https://github.com/nginx/nginx/pull/1561
- 主线版 1.31.2 易受影响基线(固定到提交 2fd01ed4)https://github.com/nginx/nginx/commit/2fd01ed47a1fd2965754c83f53b33a789d0e07f1
- 主线版 1.31.3 已修复基线(固定到提交 073ab5db)https://github.com/nginx/nginx/commit/073ab5db06ec5f5079280a60a28f450b8f1ac504
- 稳定版 1.30.3 易受影响基线(固定到提交 47c3628d)https://github.com/nginx/nginx/commit/47c3628d23efaa1bfb1a32afbe9e3d013f860c2c
- 稳定版 1.30.4 已修复基线(固定到提交 017cf98d)https://github.com/nginx/nginx/commit/017cf98dcce217946572a896f0992370475e189f
- 捕获复制的准备性简化:提交 28219209https://github.com/nginx/nginx/commit/28219209e0b4f9e155fd8bd91ab81b8ac30628f2
- 通用脚本复制边界修复:提交 b7675404https://github.com/nginx/nginx/commit/b767540492e8c79a58bc26034d3bab2f708b7bd1
- 访问日志边界修复:提交 4d32a270https://github.com/nginx/nginx/commit/4d32a2703c79f92b7a99ce3547759cc599d6f83e
- 直接脚本消费端修复:提交 25f920echttps://github.com/nginx/nginx/commit/25f920eca977139dc6fc7638b419169a602a0064
- 实际结果长度修复:提交 a8289aa6https://github.com/nginx/nginx/commit/a8289aa69c74f7e664ad63b91c17aa2a554f190f
- 稳定版准备性重写:提交 ea47fabfhttps://github.com/nginx/nginx/commit/ea47fabf55e2bc9192ddc27f52486b4e58d5e007
- 稳定版通用脚本复制边界修复:提交 7ac67898https://github.com/nginx/nginx/commit/7ac67898bdfed2140a1bedcda252074cca11f494
- 稳定版访问日志边界修复:提交 78950bdchttps://github.com/nginx/nginx/commit/78950bdcd443830bde00f6844dba3033c18862b1
- 稳定版直接消费端修复:提交 326b17b0https://github.com/nginx/nginx/commit/326b17b0038321c29252e79bf4c53d4773fcb8b5
- 稳定版实际结果长度修复:提交 97e40e59https://github.com/nginx/nginx/commit/97e40e59b7b4ad6aa36f54ce4f1e64d1e98f24cb
- NGINX Plus 官方发布记录https://docs.nginx.com/nginx/releases/
- NGINX 命令行参数官方文档https://nginx.org/en/docs/switches.html
- NGINX 进程控制与升级官方文档https://nginx.org/en/docs/control.html
- CISA KEV 官方数据镜像https://raw.githubusercontent.com/cisagov/kev-data/develop/known_exploited_vulnerabilities.json
- NVD 的 CVE-2026-42533 条目https://nvd.nist.gov/vuln/detail/CVE-2026-42533
- Ubuntu 的 CVE-2026-42533 修复映射https://ubuntu.com/security/CVE-2026-42533