一、这不是"一个危险字符串"那么简单
2026 年 9 月 10 日,GitLab 发布 19.3.2、19.2.6 和 19.1.8,修复包括 CVE-2026-85706 在内的多项安全问题。官方对该漏洞的描述非常克制:仓库提交 API 存在路径限制不当和身份验证缺失,未认证攻击者可在特定条件下读取任意文件。
一天后,CISA 将其加入已知遭利用漏洞目录(KEV),确认现实攻击已经发生。
这类漏洞容易被误解为"代码没有过滤 ../"。真正的问题更深:
-
业务代码把外部输入当成普通字段;
-
文件系统把它解释成定位对象的能力;
-
API 没有先确认调用者身份;
-
最终访问继承了 GitLab 服务进程的权限。
四个条件叠加后,请求者获得的不是自己的文件权限,而是服务端进程代为读取的能力。
二、受影响范围与修复版本
【已确认事实】 受影响的是自托管 GitLab CE/EE,版本范围如下:
| 分支 | 受影响版本 | 修复版本 |
|---|---|---|
| 18.7---19.1 | 18.7 起、低于 19.1.8 | 19.1.8 |
| 19.2 | 低于 19.2.6 | 19.2.6 |
| 19.3 | 低于 19.3.2 | 19.3.2 |
GitLab.com 已由官方完成修复,GitLab Dedicated 客户无需自行升级。其他自托管部署应升级到对应修复版本或更高安全版本。
【已确认事实】 CVSS 3.1 评分为 10.0,向量为:
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
【边界说明】 评分中的完整性影响不代表公告已经确认漏洞可直接写文件或执行命令。官方明确披露的直接能力是文件读取;其他影响需要通过泄露的凭据、配置或会话材料进一步形成。
三、技术根因一:身份验证与资源访问脱节
安全 API 至少要回答四个问题:
谁在请求?
他能访问哪个项目?
他能访问项目中的哪个对象?
最终打开的文件是否属于这个对象?
如果只在页面或上游路由中做身份验证,而真正访问文件的接口或服务方法没有重复建立安全上下文,就可能出现"入口看似受保护、数据汇点实际裸露"的情况。
【已确认事实】 GitLab 公告明确指出受影响接口存在身份验证缺失。
【工程建议】 认证和授权检查应靠近数据汇点。真正执行文件读取的方法不应仅信任调用方传来的"项目""提交"或"已验证"标记,而应使用服务端建立的身份重新验证资源关系。
四、技术根因二:路径规范化不是字符串过滤
1. 为什么删除 ../ 不可靠
文件路径可能经历 URL 解码、框架解析、平台分隔符转换、符号链接解析和挂载点映射。只在某个阶段删除一个字符串,无法证明最终路径仍在允许目录中。
真正应该验证的是:
canonical(target) ∈ canonical(allowed_root)
也就是完成所有必要解析后,目标必须仍然属于允许根目录。
2. 一个无害的概念模型
下面的代码不访问 GitLab、不发送网络请求,也不读取任何真实文件:
from pathlib import Path
def resolve_inside(root: Path, supplied: str) -> Path:
trusted_root = root.resolve()
target = (trusted_root / supplied).resolve()
if target != trusted_root and trusted_root not in target.parents:
raise ValueError("target is outside the allowed root")
return target
它体现的是安全不变量,而不是某个具体攻击载荷:
-
先完成路径解析;
-
再检查真实父子关系;
-
越界就拒绝;
-
校验后到使用前,路径对象不应被不可信方替换。
【边界说明】 这是通用 CWE-22 模型,不是 GitLab 源码或 PoC。GitLab 尚未公开足以逐行还原漏洞的完整补丁细节。
五、为什么服务账号权限决定真实影响
"任意文件"在实际系统中通常意味着"服务进程能够读取的任意文件"。因此,相同版本的两个 GitLab 实例,真实风险可能完全不同。
影响可由以下因素放大或收缩:
| 因素 | 风险较低 | 风险较高 |
|---|---|---|
| 运行身份 | 专用低权限账号 | 高权限账号或权限混用 |
| 容器挂载 | 只挂载必要目录 | 挂载主机配置、凭据目录 |
| 密钥管理 | 外部短期凭据 | 长期秘密明文落盘 |
| 网络权限 | GitLab 与下游隔离 | 可直达数据库、云和制品库 |
| 日志能力 | 完整 API 与主机审计 | 日志短期轮转或字段缺失 |
【风险推断】 如果进程可读范围包含实例秘密、数据库凭据、Runner 或第三方集成令牌,攻击者可能进一步访问下游系统。但是否真正发生,必须由日志、文件权限和凭据使用记录证明。
六、代码审计时应搜索什么
开发和安全团队不应只搜索一个 CVE 的接口名,而应审计同类模式:
1. 输入来源
-
URL 路径与查询参数;
-
JSON、表单和多段上传字段;
-
仓库元数据、提交信息和归档条目;
-
代理头、环境变量和异步任务参数;
-
数据库中曾由用户写入的路径。
2. 危险汇点
-
文件读取、复制、解压与归档;
-
模板加载、附件预览与下载;
-
Git 对象导出、缓存读取和临时文件处理;
-
通过系统命令调用外部工具处理路径;
-
将文件内容写入响应或错误信息。
3. 失效的防线
-
只检查字符串前缀;
-
只过滤
..; -
校验前后进行了不同次数的解码;
-
规范化后又把原始输入重新拼回路径;
-
只在控制器认证,内部服务可被其他路径直接调用;
-
依赖文件系统权限兜底,却给服务账号过宽权限。
七、测试重点:从样例测试升级到安全属性
建议把以下内容写成自动化回归测试:
-
未认证请求永远不能进入文件读取逻辑;
-
合法用户不能跨项目或跨提交访问对象;
-
任意输入解析后的路径必须位于允许根目录;
-
绝对路径、不同分隔符、编码变体不能改变结论;
-
符号链接不能把合法路径重定向到根目录外;
-
错误响应不能带回文件内容;
-
所有等价参数位置都执行同一校验。
可以进一步使用属性测试:随机生成路径片段、编码组合和目录树,验证安全不变量,而不是只维护几个已知恶意字符串。
八、团队现在应该做什么
P0:立即止血
-
盘点所有自托管 GitLab 节点和灾备实例;
-
升级到 19.1.8、19.2.6、19.3.2 或更高安全版本;
-
对公网入口临时增加访问限制;
-
保留 API、代理、WAF、系统和容器日志;
-
运行 GitLab 官方检测规则。
P1:确认暴露面
-
枚举 GitLab 服务账号可读目录和容器挂载;
-
识别其中的配置、令牌和连接凭据;
-
检查异常请求、登录、Runner 注册和流水线活动;
-
对已证实或无法排除的秘密执行有序轮换;
-
撤销旧会话和旧令牌,而不是只创建新值。
P2:修复设计缺陷
-
统一路径解析与允许根校验组件;
-
将认证、资源授权和路径授权放在同一调用链;
-
对文件处理接口增加属性测试和模糊测试;
-
降低服务账号权限,减少宿主机目录挂载;
-
将关键秘密迁移到短期凭据或外部秘密管理系统。
九、三个必须避免的错误结论
"这是读取漏洞,所以不会影响完整性"
读取到高权限令牌后,攻击者可能通过合法接口产生写操作。直接能力与最终影响不能混为一谈,也不能彼此割裂。
"没有公开 PoC,就没有紧迫性"
CISA 已确认在野利用。公开利用代码不是现实风险成立的前提。
"升级后不再需要调查"
补丁阻止未来请求,却无法删除攻击者已经复制走的内容。进入 KEV 后,升级和威胁狩猎应同步进行。
总结
CVE-2026-85706 的关键教训不是"过滤好路径字符串",而是建立完整的访问证明:调用者经过认证、资源关系经过授权、最终路径位于允许根目录、服务进程只拥有最小权限。
任何一层缺失,都可能把一个看似普通的文件参数变成服务器代理读取能力;当它发生在 GitLab 这样的开发基础设施中,影响就可能沿凭据和软件交付链继续扩大。