FlawOpen

flawopen.com/Incidents/gitlab-commits-api-path-traversal-september-2026

● GitLab · CVE-2026-85706 · September 2026 · CVSS 10.0 · 严重
安全研究 · FlawOpen

CVE-2026-85706:GitLab服务器文件泄露

GitLab上传解析器允许未认证请求选择服务器文件;无效转义可使片段出现在错误中。文件选择与内容泄露是不同阶段,需分别评估。[1], [3]

CVSS 向量CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N[3]
💡 通俗易懂的原理解析 (ELI5)

想象接待员本应处理收件盘里的文件,却接受访客手写的柜子位置。还没查验通行证,他就打开那个柜子;如果读不懂某一行,还会在报错时大声复述。擅自打开是一处失误,复述内容是另一处。[1]

核心概念与专有名词

Upload metadata
请求提供的 file.path 或 metadata.path 不应替代可信上传中间件登记的缓冲区。[1]
Percent decoding
表单解析会解码 % 后跟两个十六进制数字的序列;无效序列可能在报错中包含出错片段。[1]
CWE-22 / CVSS
CNA 将 CVE-2026-85706 定为路径遍历,CVSS 10.0 严重;被跨越的边界是由请求方选择服务器文件。[3]

框架映射

事件演进时间线

2026-09-10

GitLab 发布了 19.1.8、19.2.6 和 19.3.2 修复版本。[2]

2026-09-23

发布公告后来补充了 18.11.12 与 19.0.9 的回移修复。[2]

攻击执行流程分解

步骤 1

认证前选取

上传元数据可在认证前以服务器路径替换预期的临时缓冲区。[1]

步骤 2

文件被当作表单

使用 application/x-www-form-urlencoded 时,所选文件按表单数据解析,而不是作为正常提交响应返回。[1]

步骤 3

错误反射

无效的百分号转义可能把出错片段写入错误消息。& 与 = 会影响片段范围;读取并不等于泄露。[1]

步骤 4

调查核实

使用 correlation_id 关联 Rails API 与 Workhorse 日志;比较 api_error 和 written_bytes,以判断响应是否发送了字节。[1]

源代码对比:漏洞与安全实现

这是 Python 教学模型,并非 GitLab 源码或补丁。脆弱示例只使用内存缓冲区,并把任意 % 都视为错误;它不模拟真实的百分号校验或片段选择。强化版先认证,拒绝路径元数据,只接收可信上传系统登记的字节,限制大小并返回通用错误。仍须安装厂商更新:若较早的中间件已经读了文件,只在路由中认证就太晚了。[1]

存在漏洞的实现
from urllib.parse import unquote_to_bytes

def parse_upload(authenticated, metadata, buffers):
    # 无害的内存中测试数据;不会访问文件系统。
    body = buffers.get(metadata.get("path"), b"")
    if b"%" in body:
        return 400, body.decode("ascii", errors="replace")
    if not authenticated:
        return 401, "Unauthorized"
    return 200, unquote_to_bytes(body)
加固后的安全修复
from urllib.parse import unquote_to_bytes

MAX_BYTES = 4096
HEX = frozenset(b"0123456789abcdefABCDEF")

def parse_upload(authenticated, metadata, registered_body):
    # registered_body 来自可信的上传中间件,
    # 而不是请求参数选择的路径。
    if not authenticated:
        return 401, "Unauthorized"
    if metadata or type(registered_body) is not bytes:
        return 400, "Invalid upload"
    if len(registered_body) > MAX_BYTES:
        return 413, "Upload too large"
    for i, byte in enumerate(registered_body):
        if byte == 37 and (i + 2 >= len(registered_body) or
                registered_body[i+1] not in HEX or
                registered_body[i+2] not in HEX):
            return 400, "Invalid upload"
    return 200, unquote_to_bytes(registered_body)

工程与系统安全加固清单

参考来源