GitLab CE/EE 中未认证的任意本地文件读取。影响 18.7–19.1.7、19.2.0–19.2.5、19.3.0–19.3.1。已在 19.1.8 / 19.2.6 / 19.3.2 中修复(2026-09-10)。CVSS 10.0,据称已在野利用。原始报告由 s3ntago 提交,本仓库只是我的分析文章 + PoC。
本 PoC 仅出于教育和防御性研究目的发布,旨在帮助管理员和研究人员理解并测试该漏洞。请仅针对你拥有或已获得明确书面授权测试的系统运行它。如果你的实例处于受影响范围内,请停止阅读并先修补到 19.1.8 / 19.2.6 / 19.3.2。
三个仓库端点(POST :id/repository/commits、POST/PUT :id/repository/files/:file_path)位于 Workhorse 的 requestBodyUploader 之后。Rails 处理器直接从原始 file.path 请求字段读取磁盘路径,并在任何身份验证之前对其执行 File.open。require_gitlab_workhorse! 并不能作为身份验证,因为 Workhorse 的签名往返器会为其代理的每个请求附加一个有效的 Gitlab-Workhorse-Api-Request JWT,因此任何落入通用 API 代理的请求都会通过该检查。
这之所以没有立即变成对所有人的 LFI,唯一原因是 Workhorse 本应首先重写请求。但它的路由正则匹配的是转义后的路径(EscapedPath() 加上一个从不解码 %XX 的 path.Clean 克隆),而 Puma 在 Grape 路由之前会解码 %XX。因此,对静态段中的任意字符进行百分号编码(%63ommits、%72epository、%66iles),添加尾部斜杠,或附加 .json,Workhorse 的正则就会漏掉,而 Rails 仍会路由到存在漏洞的处理器。这种编码不匹配正是绕过点所在。(//、/./、%2F、; 这类变体不起作用,因为 path.Clean 会规范化前两者,而 Puma 会拒绝 %2F。)
然后只需将伪造的未签名上传元数据作为查询参数发送:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file= 为空可满足 requires :file, WorkhorseFile 验证(空值会被强制转换为 nil)。读取在身份验证之前触发。把字节取回来才是有趣的部分:在 urlencoded 分支上,辅助函数会执行 Rack::Utils.parse_nested_query(File.read(path)),并将解析器错误插值到 400 响应体中。任何后面没有跟两个十六进制数字的 % 都会引发 InvalidParameterError: invalid %-encoding (<raw file bytes>),文件内容会随错误消息一起返回。JSON 分支(Oj)不会泄露任何内容,这就是此处 urlencoded 内容类型很重要的原因。
修复(master 0d9ce3e7,向后移植 1fe30154 / b43c8b26 / 0ff7b6b2)为所有三个端点以及 /authorize 前置步骤添加了 authenticate!,仅信任中间件生成的 UploadedFile 来获取路径/大小,并停止回显解析器错误。该 bug 于 2025 年 12 月引入,这就是受影响范围从 18.7 开始的原因。
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
该脚本会遍历所有已验证的绕过形式(编码段、尾部斜杠、.json;commits + files 端点,POST 和 PUT),并对每个响应进行分类,以便你能判断探测在链条中的哪个位置失败。显然,仅限授权目标。
% 时,回显才会触发。(.*) 一直到响应体中的最后一个 ))。针对标准 JSON 错误没问题,但如果前面有什么东西把响应包装在 HTML 中,就会过度捕获。正确的修复方法是解析 JSON 消息字段。before 块会返回 404)。