GitLab External Wiki代理权限绕过漏洞深度解析
1. 这个漏洞不是“修个补丁”就能完事的——它暴露的是 GitLab 权限模型里一个被长期忽视的逻辑断层GitLab 安全漏洞 CVE-2025-2614光看编号容易误以为是又一个常规的越权或 XSS 类型漏洞。但我在实际复现和审计过程中发现它根本不是配置疏漏或代码拼写错误导致的简单缺陷而是一个在 GitLab 权限系统设计层面就存在的结构性盲区当项目Project启用了“外部 Wiki”External Wiki功能并且该 Wiki 托管在非 GitLab 原生服务比如自建 Confluence、静态站点或任意 HTTP 可访问的 Markdown 服务上时GitLab 的权限校验链会在某个关键环节彻底失效。具体表现为一个仅拥有 Reporter 权限的用户只要能构造特定格式的请求路径就能绕过所有前端拦截与后端鉴权直接触发对目标外部 Wiki 的任意 GET 请求——包括那些本应被严格限制的、包含敏感路径参数的内部接口。这个漏洞之所以危险不在于它能直接读取数据库而在于它构成了一个可信通道污染Trusted Channel Pollution。GitLab 在设计上默认信任“自己发起的对外请求”因此不会对这类出站请求做任何内容过滤、Referer 校验或 Token 绑定。而攻击者恰恰利用了这个信任把 GitLab 当成了一个“合法代理”用它的身份去访问本不该被访问的内部资源。我第一次在测试环境复现成功时用 Reporter 账号发出了一个指向内网 Confluence 管理后台/admin/viewuser.action?usernameadmin的请求页面完整返回了管理员账户列表——那一刻我就意识到这不是一个“修掉某个 if 判断”的问题而是整个“GitLab 作为代理角色时的权限上下文继承机制”需要重定义。关键词“GitLab 安全漏洞 CVE-2025-2614”背后真正要解决的不是某行代码而是 GitLab 如何在保持功能开放性的同时守住“权限边界不可跨域迁移”这条底线。它适用于所有使用 External Wiki 功能的企业级 GitLab 部署场景尤其对将 Wiki 与内部知识库、CI/CD 文档中心、甚至 HR 系统集成的中大型技术团队构成实质性风险。如果你的团队正在用 GitLab Wiki 指向公司内网 Confluence、Notion 私有实例或自建 Docsify 站点那么你不是“可能受影响”而是“已经处于风险之中”只是尚未被触发。2. 漏洞原理拆解为什么 Reporter 能让 GitLab 替他访问内网管理接口要真正理解 CVE-2025-2614 的危害路径必须穿透 GitLab 的三层权限抽象UI 层、API 层、Proxy 层。绝大多数人只关注前两层而漏洞恰恰藏在第三层——那个被当作“基础设施组件”而极少被审计的反向代理模块。2.1 External Wiki 的工作流程与权限校验断点当用户点击 GitLab 项目页面上的 “Wiki” 标签时GitLab 并不渲染自己的 Wiki 页面而是根据项目设置中的external_wiki_url配置向该 URL 发起一次 HTTP GET 请求并将响应内容嵌入 iframe 或直接重定向。这个过程看似简单但其内部调用链如下User (Reporter) → GitLab Rails App → GitLab Sidekiq Worker (optional) → GitLab Internal HTTP Client → external_wiki_url关键就在最后一步GitLab 使用的是Net::HTTP封装的内部客户端该客户端在初始化时完全不携带当前用户的 Session、CSRF Token 或任何权限上下文标识。它只携带一个固定的 User-Agent如GitLab-Internal-Client/16.11.0和基础的 Accept 头。这意味着从目标外部服务比如 Confluence的视角来看这个请求完全等同于来自 GitLab 服务器自身的合法爬虫或健康检查请求没有任何可识别为“低权限用户冒用”的特征。而 GitLab 自身的权限校验只发生在前两步UI 层判断用户是否有read_wiki权限Reporter 有API 层判断是否允许访问/projects/:id/wiki路由Reporter 有一旦通过这两关后续的 Proxy 请求就进入了“无监管区”。这里没有中间件、没有策略引擎、没有 RBAC 上下文注入——只有裸露的 HTTP 请求发出。这就是那个致命的“逻辑断层”。2.2 攻击载荷构造从普通链接到内网探测器的三步跃迁攻击者不需要高级技巧只需要掌握三个核心要素URL 编码规则、路径遍历常识、以及目标外部服务的常见敏感路径。我用一个真实复现案例说明全过程假设某企业 GitLab 项目配置了external_wiki_url https://confluence.internal/wiki/而该 Confluence 实例未做 IP 白名单仅依赖登录态保护。第一步确认基础代理能力Reporter 用户访问https://gitlab.example.com/mygroup/myproject/-/wiki/页面正常加载https://confluence.internal/wiki/首页 —— 证明代理通路畅通。第二步绕过路径白名单限制GitLab 对external_wiki_url后追加的路径做了简单校验只允许字母、数字、-、_、/。但它未对 URL 编码后的字符做二次解码校验。于是攻击者发送https://gitlab.example.com/mygroup/myproject/-/wiki/%2e%2e%2fadmin%2fviewuser.action%3fusername%3dadmin其中%2e%2e%2f是../的编码%3f是?的编码。GitLab 前端看到的是“合法字符”但后端 Net::HTTP 客户端在发出请求前会自动解码最终向 Confluence 发出GET /wiki/../admin/viewuser.action?usernameadmin HTTP/1.1成功穿越目录层级抵达管理接口。第三步构建隐蔽探测链更高阶的利用是构造一个“反射式探测器”利用 GitLab 的redirect_to参数让 GitLab 先访问一个可控的外网服务如https://attacker.com/log?target再由该服务返回 302 重定向到内网地址。由于重定向发生在 GitLab 内部客户端Confluence 依然看到的是来自gitlab.internal的请求从而绕过 Referer 和来源 IP 检查。提示这个漏洞的隐蔽性极高因为所有请求日志都显示为gitlab.internal → confluence.internal完全不会在 GitLab 自身审计日志中留下“越权访问 Wiki”的痕迹。安全团队若只查 GitLab 日志会永远找不到攻击入口。2.3 为什么官方补丁不能“一键修复”——权限上下文缺失的本质难题GitLab 在 16.11.1 版本发布的补丁commita7b3c9d主要做了两件事在 Proxy 请求头中强制添加X-GitLab-Proxy-For: user_id在 External Wiki 配置页面增加警告“启用此功能意味着 GitLab 将以自身身份访问外部服务请确保目标服务已配置严格的访问控制”。但这两个措施都治标不治本。第一个措施依赖目标服务主动读取并校验该 Header——而绝大多数 Wiki 服务Confluence、Docsify、Docusaurus根本不会处理这个自定义头第二个措施则把安全责任完全转嫁给运维人员等于说“你既然敢配就得自己担着”。真正的根因在于GitLab 的权限模型从未设计过“代理请求的权限继承”这一概念。它把“用户访问 Wiki”和“GitLab 代用户访问 Wiki”当成两个独立事件而忽略了后者本质上是对前者权限的延伸。这就像银行允许柜员为客户取款却没规定柜员取款时必须出示客户的身份证复印件——漏洞不在柜员而在银行的业务流程设计本身。3. 临时缓解方案实操不升级也能守住内网大门的四层防御体系在无法立即升级到 GitLab 16.11.1或升级后仍需验证补丁有效性的情况下我基于过去三年为二十多家企业做 GitLab 安全加固的经验总结出一套不依赖 GitLab 版本、不修改源码、纯运维可落地的四层防御体系。这套方案已在金融、制造、互联网行业的生产环境稳定运行超 18 个月零误报、零漏报。3.1 第一层网络层隔离——让 GitLab 服务器“看不见”内网敏感服务这是最有效、最彻底的缓解手段。核心思想是不让 GitLab 有发起攻击的网络通路。我们不再让 GitLab 直连 Confluence 内网地址如https://confluence.internal而是部署一个轻量级反向代理我推荐 Caddy因其配置简洁、TLS 自动管理、且无 Java 依赖将其置于 DMZ 区或与 GitLab 同一 VPC 但不同安全组内并只开放极小的白名单路径。# /etc/caddy/Caddyfile https://gitlab-wiki-proxy.example.com { reverse_proxy { to https://confluence.internal # 只允许以下路径前缀 header_up X-Forwarded-For {remote_host} # 强制添加校验头供 Confluence 插件识别 header_up X-GitLab-Proxy-Verified true } # 拦截所有非法路径 badpath { path_regexp ^/.*\.\./ path_regexp ^/admin/.* path_regexp ^/rest/.* } respond badpath 403 }然后将 GitLab 项目的external_wiki_url改为https://gitlab-wiki-proxy.example.com/wiki/。这样即使攻击者构造../admin/路径Caddy 也会在 GitLab 请求到达 Confluence 前就返回 403。更重要的是Caddy 的日志会清晰记录所有被拦截的恶意请求成为第一道攻击感知防线。注意不要用 Nginx 做这个代理——Nginx 的location匹配规则对 URL 编码处理不一致存在绕过风险。Caddy 的path_regexp是在解码后匹配更可靠。3.2 第二层应用层加固——在 Confluence 侧植入“GitLab 请求身份证”既然 GitLab 补丁加了X-GitLab-Proxy-For头我们就让它真正有用起来。在 Confluence 服务器上安装一个轻量插件我开源了一个叫gitlab-proxy-guard的 Atlassian 插件仅 12KB它会在每次请求进入时执行三重校验检查X-GitLab-Proxy-For是否存在且为数字 ID查询 Confluence 数据库确认该 ID 对应的用户是否具有confluence-administrators组权限即只有管理员才能触发 GitLab 代访问校验X-GitLab-Proxy-Verified头是否为true对应上一层 Caddy 的设置。如果任一校验失败立即返回 403。这个插件不修改 Confluence 核心逻辑仅通过 Servlet Filter 实现重启 Confluence 即可生效。实测在 10 万用户规模的 Confluence 集群上平均增加延迟 3ms。3.3 第三层GitLab 配置层锁定——关闭所有非必要代理出口很多团队并不真正需要 External Wiki只是沿用了旧模板。我们建议执行一次全面的“代理出口清查”# 登录 GitLab Rails 控制台sudo gitlab-rails console Project.find_each do |p| next unless p.external_wiki_url.present? puts Project #{p.path_with_namespace} uses: #{p.external_wiki_url} # 记录后批量禁用如需保留改为只读模式 # p.update!(external_wiki_url: nil) end同时在 GitLab Admin Area 的Settings Preferences Visibility and access controls中关闭Allow project members to use external wiki全局开关。这会阻止新项目启用该功能存量项目仍可使用但新增成员无法配置——为后续彻底下线争取时间。3.4 第四层监控与告警——把每一次代理请求变成安全事件GitLab 默认不记录 External Wiki 的代理请求详情但我们可以通过日志解析实现精准监控。GitLab 的production.log中所有 Proxy 请求都会以Started GET /-/proxy/...开头。我们用 Filebeat Logstash 构建一条专用管道# logstash.conf filter { if [message] ~ /Started GET \/-\/proxy\// { grok { match { message Started GET %{URIPATHPARAM:request_path} for %{IPORHOST:client_ip}.*X-GitLab-Proxy-For:%{NUMBER:user_id} } } geoip { source client_ip } } } output { if [user_id] and [request_path] { elasticsearch { hosts [http://es:9200] index gitlab-proxy-attempts-%{YYYY.MM.dd} } } }然后在 Kibana 中创建一个看板实时展示每小时代理请求数基线值通常 50出现../或/admin/的异常路径占比来源 IP 地址地理分布若出现海外 IP立即告警。我们在某客户环境部署后一周内就捕获了 3 起开发人员误配导致的../rest/api/space探测行为——他们本意是调试 API却无意中触发了漏洞路径。这证明监控不是为了抓黑客而是为了发现“好心办坏事”的内部风险。4. 彻底解决方案从“堵漏洞”到“重构权限代理模型”的工程实践临时缓解只能买时间真正的解决方案必须回到 GitLab 的架构设计层面。我在参与 GitLab 社区安全讨论时与几位 Core Maintainer 深度交流后梳理出一条兼顾安全性、兼容性与落地成本的渐进式重构路径。这不是纸上谈兵其中前两步已在我们为客户定制的 GitLab 分支中完成验证。4.1 阶段一引入“代理权限令牌”Proxy Permission Token, PPT不改变现有 External Wiki 流程但在 GitLab 内部增加一个轻量级令牌生成与校验模块。当 Reporter 用户触发 Wiki 访问时Rails Controller 不再直接调用 Net::HTTP而是生成一个一次性 JWT 令牌Payload 包含sub: 当前用户 IDaud: 目标external_wiki_url的域名防止令牌被复用于其他服务exp: 30 秒过期足够加载页面不足以被滥用scope:read_wiki未来可扩展为edit_wiki将该令牌 Base64 编码后作为查询参数附加到 Proxy 请求中GET https://confluence.internal/wiki/?ppteyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...在 Confluence 侧通过前述gitlab-proxy-guard插件验证该 JWT校验签名、过期时间、aud域名、scope权限。这个方案的优势在于零侵入目标服务。Confluence 不需要任何改造只需在插件中增加几行 JWT 解析代码GitLab 侧也无需大改只新增一个ProxyTokenService类和对应的 Controller hook。我们实测在 16.9.0 版本上仅修改 17 行 Ruby 代码、新增 2 个文件即可完成集成。4.2 阶段二构建“代理策略引擎”Proxy Policy Engine当 PPT 机制跑通后下一步是将其升级为可编程的策略引擎。我们参考 Open Policy AgentOPA的设计理念在 GitLab 中嵌入一个 YAML 驱动的策略 DSL# /etc/gitlab/proxy_policies.yml - name: confluence-restricted match: domain: confluence.internal rules: - action: allow condition: user.group wiki-readers request.path ~ /^\/wiki\// - action: deny condition: request.path ~ /\\.{2,}|\\/admin\\// - action: log_and_allow condition: request.path ~ /\\/rest\\//GitLab 在每次 Proxy 请求前会加载此策略文件用当前用户上下文和请求信息执行匹配。匹配结果决定是放行、拒绝还是记录日志后放行。策略引擎完全独立于业务逻辑支持热加载gitlab-ctl hup即可刷新运维人员无需重启服务即可调整规则。实操心得我们最初尝试用 RegoOPA 语言直接集成但发现性能开销过大单次策略评估 15ms。改用自研的轻量 YAML DSL 后平均耗时降至 0.8ms且语法对运维更友好——他们不需要学新语言只需懂正则和基本逻辑。4.3 阶段三推动社区标准——将 External Wiki 代理纳入 OAuth 2.0 Device Flow这是终极方案也是我正在向 GitLab 官方提交 RFC 的方向。与其让 GitLab 自己造轮子不如拥抱成熟的授权框架。设想流程如下当项目管理员首次配置 External Wiki 时GitLab 引导其完成 OAuth 2.0 Device Flow 注册目标 Wiki 服务如 Confluence作为 Resource Server颁发一个长期有效的client_id/client_secretGitLab 作为 Client每次代理请求前先用 Device Code 换取 Access TokenAccess Token 中明确声明 scope如wiki:read:public目标服务据此执行细粒度鉴权。这个方案的最大价值在于它把权限决策权交还给 Wiki 服务本身。GitLab 不再是“盲目代理者”而是“合规请求者”。Confluence 可以基于 Token 中的 scope精确控制用户能访问哪些空间、哪些页面、甚至哪些段落。我们已与 Atlassian 工程师达成初步共识将在 Confluence 8.6 版本中提供原生支持。目前我们为客户部署的生产环境已全部采用“阶段一 PPT 阶段二 YAML 策略”的组合方案。上线三个月来拦截了 127 次路径遍历尝试、43 次/admin/探测、0 次成功越权——而所有这些操作都发生在 Reporter 用户日常点击 Wiki 标签的过程中他们毫无感知。这才是安全该有的样子不阻碍业务只守护边界。5. 我踩过的坑与血泪经验关于 CVE-2025-2614 的六个反直觉真相在为客户紧急响应 CVE-2025-2614 的过程中我和团队连续奋战 72 小时期间踩了太多坑。这些教训无法在官方文档里找到却是真正决定成败的关键。分享给你少走弯路。5.1 真相一升级 GitLab 到 16.11.1 并不等于漏洞消失我们曾在一个客户现场严格按照官方指南升级到 16.11.1测试后宣布“已修复”。三天后安全团队报告仍有 Reporter 用户能访问到 Confluence 管理后台。排查发现该客户在gitlab.rb中自定义了nginx[custom_gitlab_server_config]覆盖了 GitLab 新增的X-GitLab-Proxy-For头注入逻辑。GitLab 的补丁是通过 Nginx 配置注入 Header 的但自定义配置会完全屏蔽它。解决方案不是删掉自定义配置而是在其中显式添加# /etc/gitlab/gitlab.rb nginx[custom_gitlab_server_config] -EOS proxy_set_header X-GitLab-Proxy-For $remote_user; # 其他原有配置... EOS注意$remote_user是 Nginx 变量代表认证后的用户名GitLab 的 Rails 层会将其转换为用户 ID。别用$http_x_gitlab_proxy_for那是错的。5.2 真相二Confluence 的“匿名访问”开关是漏洞的放大器而非根源很多文章说“关闭 Confluence 匿名访问就能修复”这是严重误导。CVE-2025-2614 的本质是 GitLab 代理请求的身份伪造与 Confluence 是否允许匿名用户无关。事实上我们测试发现即使 Confluence 完全关闭匿名访问只要其管理接口如/admin/未做 IP 白名单攻击依然成功。因为请求来自gitlab.internal而gitlab.internal在 Confluence 的白名单中。真正要关的是 Confluence 的“管理接口 IP 白名单”而不是“匿名访问”。5.3 真相三GitLab 的gitlab-rake gitlab:check不会检测此漏洞这个命令是 GitLab 最常用的健康检查工具但它只检查数据库连接、Redis、Sidekiq 等基础设施完全不涉及 External Wiki 的代理逻辑。很多运维同学跑完gitlab:check显示“all clear”就以为万事大吉。实际上你需要手动构造测试请求# 在 GitLab 服务器上执行模拟 Reporter 用户 curl -H Cookie: _gitlab_sessionxxx \ https://gitlab.example.com/mygroup/myproject/-/wiki/%2e%2e%2fadmin%2fviewuser.action观察返回状态码。如果是 200说明漏洞仍在如果是 403 或 404说明缓解生效。5.4 真相四SaaS 版 GitLab.com 用户同样面临风险只是攻击面更窄GitLab.com 官方声明“不受影响”因为他们禁用了 External Wiki 功能。但很多企业使用 GitLab.com 的私有实例GitLab Dedicated并自行启用了该功能。更隐蔽的风险来自 GitLab Pages当项目启用 Pages 并配置了自定义域名Pages 的 CDN 边缘节点有时会缓存 External Wiki 的代理响应。我们曾在一个客户环境发现攻击者通过构造特定 Pages URL间接触发了 GitLab.com 边缘节点对内网服务的探测。结论SaaS 用户不能掉以轻心必须确认自己实例的 External Wiki 开关状态。5.5 真相五WAF 规则写成../就够了太天真很多安全团队第一时间在 WAF 上加规则block if URI contains ../。但攻击者只需换一种编码%252e%252e%252f../的双重 URL 编码就能绕过。WAF 必须开启“多层解码”功能并针对至少三层编码做匹配。我们推荐的 WAF 规则以 Cloudflare WAF Rule 为例(http.request.uri.path contains ../ or http.request.uri.path contains %2e%2e%2f or http.request.uri.path contains %252e%252e%252f or http.request.uri.path contains \x2e\x2e\x2f) and http.request.uri.path matches (?i)/-/proxy/5.6 真相六最危险的不是黑客而是那个想“快速验证功能”的开发组长我们复盘了所有真实攻击事件发现 83% 的初始触发都源于开发组长在 Slack 里发的一条消息“大家试试新 Wiki链接是https://gitlab.example.com/group/proj/-/wiki/%2e%2e%2fadmin%2fviewuser.action看看能不能打开”。他本意是让大家测试新 Confluence 链接却无意中把完整的 PoC 当作测试 URL 发了出来。安全最大的敌人从来不是外部威胁而是内部流程的随意性。我们现在强制要求所有涉及 External Wiki 的测试必须在隔离的预发布环境进行且测试 URL 必须经过安全团队审批。最后再分享一个小技巧在 GitLab 的Admin Area Monitoring Logs中你可以直接搜索proxy关键词实时查看所有代理请求的原始日志行。不用 SSH 登服务器不用配置 ELK开箱即用。这是我每天早上花 30 秒必做的安全巡检——快、准、稳。