漏洞
GitLab 满分漏洞,错在正则编译的那一步
CVE-2026-97229 影响 GitLab CE/EE 的三个维护分支,特定条件下可能经 CI/CD 正则编译触发未认证服务器代码执行,厂商评分为 10.0。当前应安装含运行时修补的完整 GitLab 包,更新到 19.2.8、19.3.5 或 19.4.2,不能只更新 Runner。

文章导航
检查一份流水线配置时,人们通常先看它准备执行哪些命令。正则表达式看起来只是筛选条件,决定某个分支、文件或变量是否匹配。可是在执行匹配之前,正则还要被编译成程序。这一步本身就会计算长度、生成指令和写入内存。
10 月 12 日,GitLab 发布紧急安全公告。其中的 CVE-2026-97229 发生在 CI/CD 正则编译环节,厂商称特定条件下,未登录的攻击者可以通过特制正则在 GitLab 服务器上执行代码。GitLab 给出的 CVSS 3.1 分数是 10.0,向量中的 PR:N 和 UI:N 分别表示不需要既有权限和用户交互,完整远程触发请求仍未公开。
自托管管理员现在要更新的是 GitLab 产品及其随包运行时。只更新 Runner,改变不了服务器上编译正则的代码。当前官方目标是 19.2.8、19.3.5、19.4.2。尤其要留意 19.3 分支,一些报道仍写 19.3.4,现行公告已改用 19.3.5,并解释前一个构建在启用 Ona 集成时会影响合并请求页面。GitLab.com 已由厂商修补,Dedicated 客户无需自行处理这批升级。
1 短规则也会展开成很长的程序
正则编译器需要把“重复若干次”变成能够执行的指令。对足够小的重复,直接复制指令可以省去循环控制。对很大的重复,保留循环通常更合适。于是,编译器先估算展开后的长度,再决定选哪种形式。
GitLab 本批发行回补的 Ruby 程序长度修补来自上游 PR #19020,它展示了这个选择如何出错。完整 GitLab 请求入口尚未披露,先看 Ruby 上游修补前后的源码。在上游修补前的 regcomp.c 第 983–1047 行,compile_length_quantifier_node() 计算重复节点需要的长度。tlen 是内部子表达式的编译长度,qn->lower 是最少重复次数。某些分支用两者的乘积与 QUANTIFIER_EXPAND_LIMIT_SIZE 比较,阈值为 50。
// 修补前:用于决定是否展开的一项长度判断
tlen * qn->lower <= QUANTIFIER_EXPAND_LIMIT_SIZE
两个数各自合法,乘起来仍可能超出 int 能表示的范围。嵌套重复会放大内部长度,外层重复次数再乘一次。原本用于阻止巨量展开的比较,此时依赖一个已经溢出的结果。
把数字缩到 8 位来演示,就容易看清这个错误。长度 20,重复 13 次,真实乘积是 260,明显超过 50。若只保留乘积的低 8 位,结果却是 4,比较便成了“4 不超过 50”,错误放行。修补后的比较直接算 20 <= 50 / 13,整数除法得到 3,正确拒绝展开。这是安全的截位算术模型,真实 C 程序的有符号溢出属于未定义行为,具体结果由构建条件决定。
这里的代价也不只在估算阶段。同一旧版本第 1084–1105 行的 compile_quantifier_node() 再作这一判断,进入分支后调用 compile_tree_n_times(),实际生成重复的指令。一次错误选择就把“用循环表达重复”变成了“大量复制程序”。
Ruby 上游的修补说明还指出另一条增长路径。把很多大型重复顺次连接起来,也能让最终程序超过 INT_MAX,并使写进程序的相对跳转偏移溢出。输入文件有多长、编译后程序有多长、偏移类型能表示多远,是三个不同的量。限制第一个量,不能直接保证后两个量仍有效。
2 修补分两处,先改计算,再限制实际输出
第一处修补保留原来“小重复可以展开”的设计,改变的是比较方法。修补后的第 747–751 行加入 IS_EXPAND_LIMIT_OK。重复次数为正时,比较变成子表达式长度是否小于等于“阈值除以次数”,不再先计算可能溢出的乘积。宏在 n <= 0 时短路,不执行除法。
#define IS_EXPAND_LIMIT_OK(tlen, n) \
((n) <= 0 || (tlen) <= QUANTIFIER_EXPAND_LIMIT_SIZE / (n))
对于非负长度和正的重复次数,两种写法表达同一个大小关系,除法形式却不需要先造出大乘积。这个宏同时进入长度估算和实际编译的对应分支。只改其中一边,会让估算与生成指令采用不同的展开决定。
第二处修补处理最终程序的总长度。add_opcode() 第 223–232 行在写入操作码前检查 reg->used,超限就返回 ONIGERR_TOO_BIG_COMPILED_PROGRAM。这次检查盯着已经生成的真实程序,不再信任可能同样溢出的预估长度。
if (reg->used > MAX_COMPILED_PROGRAM_SIZE)
return ONIGERR_TOO_BIG_COMPILED_PROGRAM;
BBUF_ADD1(reg, opcode);
MAX_COMPILED_PROGRAM_SIZE 定义为 INT_MAX_LIMIT / 4。补丁中的注释说明,程序偏移使用 RelAddrType,长度累计也使用 int,保留四分之三的余量是为了后续偏移和长度相加。它是这段实现的数值上限,管理员不应把它当作适用于自己服务器的内存预算。
上游还增加了嵌套重复的回归测试,用独立进程和超时限制编译过程,再检查对短文本的匹配结果。上面的本地验证只计算整数,没有运行大规模正则或完整 GitLab 利用。

tlen 是子表达式的编译长度,n 是重复次数。50 用于决定是否展开,实际输出上限另由 INT_MAX_LIMIT / 4 定义。查看大图3 单看 Ruby 版本号,看不出包里的回补
GitLab 的发行源码把上游补丁与安装包连接了起来。CNG 是 GitLab 的云原生镜像构建项目,其合并请求 !3128把程序长度修补回补到 19.4 分支的 Ruby 3.3 与 3.4。正式 v19.4.2-ee 构建提交中的 regexp-program-size.patch包含上面两处变化。
同一提交的 gitlab-ruby/Dockerfile 第 65–73 行先下载 Ruby 源码,再调用 apply_ruby_patches.sh,随后编译,文件中的默认版本是 Ruby 3.3.12。该脚本第 17–40 行先检查必需补丁是否存在,再依次应用对应 Ruby 主次版本及精确版本目录中的补丁。因此,仅看 ruby -v 的上游版本号,分不清是否含有 GitLab 的回补。
Linux 安装包也有相应步骤。正式 19.4.2+ee.0 的 config/software/ruby.rb 第 136–150 行为 Ruby 3.3 和 3.4 加入 regexp-program-size,然后按实际 Ruby 分支选择补丁文件。应当核对完整 GitLab 包或镜像的版本与构建来源,不能用替换一个 Ruby 可执行文件来代替产品更新。
同批发行还列出一个名称相近的修补,CNG !3131 的重复范围 double-free 修补。它来自另一个 Ruby 上游变更,修的是解析错误时同一节点被释放两次,方法是调整临时指针的所有权。程序长度溢出涉及展开选择与跳转偏移,重复释放涉及错误路径中的对象生命周期。安装包同时处理了这两类问题,根因需要分别理解。
GitLab CE 19.4.1 的常规 CI 表达式封装调用 UntrustedRegexp::RubySyntax,语法转换后由 UntrustedRegexp 构造 RE2 正则。公开报告尚未说明哪一入口触发 Ruby 编译器,具体漏洞报告仍按 GitLab 修补后 90 天公开的规则保留。
4 19.3 分支现在应装 19.3.5
GitLab 发布的 CNA 记录把受影响范围限定在下列三个维护分支。公告日期是 10 月 12 日,CNA 记录在 10 月 13 日 01:05 UTC 发布。以下安装目标按 10 月 13 日的现行官方公告核对。
| 现有分支 | 受影响范围 | 当前修补目标 |
|---|---|---|
| 19.2 | 19.2.0 至 19.2.7 | 19.2.8 |
| 19.3 | 19.3.0 至 19.3.4 | 19.3.5 |
| 19.4 | 19.4.0 至 19.4.1 | 19.4.2 |
19.3.4 在启用 Ona 集成时会影响合并请求页面,19.3.5 修正这项业务回归,也是现行公告与 CNA 指定的目标。已经先行安装 19.3.4 的实例继续更新到这个替代包。
升级要包含迁移的时间。官方升级说明指出这批更新含数据库迁移,单节点实例需要停机等待迁移完成,多节点可以依照其零停机流程处理。19.3.5 还包含部署后迁移。把“补丁版本”理解为无需停机或无需迁移,会让应急安排失真。
不能立即更新时,可以先把受影响 GitLab 的网络访问收束到可信管理与业务来源。这是临时隔离,会影响外部协作、Webhook 和需要访问实例的作业。公开资料没有给出覆盖完整触发链的 WAF 规则,也没有把停用某一个 CI 功能列为通用修复。升级失败的节点继续保持隔离,回退必须使用已修补且与备份兼容的包,涉及数据库回退时按官方备份恢复流程执行。
5 验收要看到新构建正在服务
升级后,先确认处理请求的各个 Web 与后台节点已经运行目标构建,旧容器、旧发布目录和没有重启的进程都要核对。再检查数据库与部署后迁移状态,走一次正常的流水线创建、规则判断和任务执行。启用 Ona 的 19.3 部署还应实际打开合并请求页面,确认这次替代构建解决了相应业务问题。正常规则和合法作业能继续工作,才有条件撤掉为修补增加的访问限制。
过去是否已经遭到利用,需要另外调查。GitLab 服务器可能持有代码、仓库访问能力和 CI/CD 凭据,确认服务器受控时,应按它实际能够接触的对象处理,而不能只看某台 Runner 上有没有异常任务。保存代理、应用与主机日志,核对异常进程、未经授权的项目或配置变更,再结合服务器可接触范围与调查结果轮换凭据、恢复可信构建。
正则编译出来的是供引擎解释的匹配指令,编译长度和跳转偏移的错误会发生在处理输入的进程内。上面的公开修补在选择展开和生成程序时检查大小。GitLab 公告把 CVE-2026-97229 的后果列为服务器代码执行,管理员要更新的因此是正在运行的完整 GitLab 构建,以及随包修补的编译器。