当前位置: 首页 > news >正文

GitLab CVE-2025-6948:CI/CD配置权限绕过漏洞深度解析

1. 这个漏洞不是“修个补丁就完事”的普通问题

GitLab CVE-2025-6948——光看编号,很多人第一反应是:“又一个中危漏洞,等官方发个补丁打上就行”。我去年在给三家制造业客户做 DevOps 安全加固时,也这么想。直到我们团队在灰度环境里复现了它:一个未授权的普通项目成员,仅通过构造特定的 API 请求路径,就能绕过权限校验,读取到本应严格隔离的 CI/CD 流水线配置文件(.gitlab-ci.yml的原始内容),进而获取其中硬编码的密钥变量(如AWS_ACCESS_KEY_IDDOCKER_REGISTRY_TOKEN)明文。这不是理论风险,是实打实的凭证泄露链路。更关键的是,它不依赖任何用户交互,不触发审计日志中的典型异常行为(比如高频失败登录),常规 WAF 规则和 SIEM 告警几乎完全失灵。这个漏洞的核心价值,不在于它多“高危”,而在于它精准击中了 GitLab 权限模型中最隐蔽的断层带——项目级访问控制(Project-level Authorization)与流水线上下文执行权限(Pipeline Context Permission)之间的语义鸿沟。它适合两类人深度参考:一类是正在搭建企业级 GitLab 私有化平台的 SRE 和安全工程师,另一类是负责 CI/CD 流水线设计、经常在.gitlab-ci.yml中管理敏感凭证的开发负责人。如果你只是个人开发者用 GitLab.com 免费版,官方已自动修复;但如果你用的是自托管 GitLab(尤其是 16.11.x 至 17.2.3 版本),这篇就是你今晚必须读完的排查清单。

2. 漏洞本质:权限校验的“盲区”不在代码里,而在 GitLab 的资源抽象层

2.1 为什么传统权限模型会失效?从 GitLab 的资源树说起

要真正理解 CVE-2025-6948,得先看清 GitLab 是怎么“看”一个请求的。GitLab 不是简单地判断“用户 A 能不能访问项目 B”,而是把整个系统拆解成一棵资源树(Resource Tree):最顶层是Group,往下是Project,再往下是PipelineJobArtifactVariable等。每个资源节点都有自己的权限策略(Policy),而策略的执行依赖于一个关键对象——Ability实例。这个实例在每次请求进入时,由CanCanCangem 初始化,并根据当前用户角色(Maintainer/Developer/Reporter)和资源类型动态加载规则。问题就出在这里:当请求目标是/api/v4/projects/:id/pipelines/:pipeline_id/jobs/:job_id/trace这类路径时,GitLab 的默认Ability规则只校验了PipelineJob层级的读取权限(:read_pipeline,:read_job),却完全跳过了对Pipeline所属的.gitlab-ci.yml配置文件本身的访问校验。因为配置文件在 GitLab 内部被抽象为ProjectFile资源,而ProjectFile的读取权限(:read_project_file)默认只在 Web UI 的“代码浏览”页面显式调用,API 路径中从未触发。这就像一栋大楼的门禁系统只检查你有没有进电梯的权限,却忘了确认你按下的楼层按钮是否属于你被授权访问的区域——漏洞就藏在这段“未被检查的路径”里。

2.2 复现的关键参数:不是 URL 路径,而是请求头里的“上下文欺骗”

很多团队在复现时卡在第一步:明明按 CVE 描述构造了/api/v4/projects/123/pipelines/456/jobs/789/trace,返回却是 403。原因在于,GitLab 在 17.0 版本后引入了X-GitLab-Feature-Flag请求头作为内部功能开关,而 CVE-2025-6948 的触发链路恰好依赖一个被标记为disabled_by_default的实验性特性:ci_pipeline_config_access_control。这个特性在默认关闭状态下,会跳过对PipelineConfig资源的显式校验。所以真正的复现命令不是简单的 curl,而是:

curl -X GET \ "https://your-gitlab.example.com/api/v4/projects/123/pipelines/456/jobs/789/trace" \ -H "PRIVATE-TOKEN: your_user_token" \ -H "X-GitLab-Feature-Flag: ci_pipeline_config_access_control=disabled"

注意两个细节:第一,PRIVATE-TOKEN必须是一个拥有Reporter或以上权限的普通用户 Token(Maintainer 权限反而可能因其他策略拦截而失败);第二,X-GitLab-Feature-Flag头的值必须精确为ci_pipeline_config_access_control=disabled,少一个字符或大小写错误都会导致校验逻辑走回正常路径。我第一次复现失败,就是因为把disabled写成了disable——GitLab 的 Feature Flag 解析器是严格字符串匹配,不支持模糊匹配。这个细节在官方公告里被刻意淡化,但却是能否稳定复现的分水岭。

2.3 影响范围远超“读取配置”:它打开了三重连锁泄露通道

很多人以为,这个漏洞最多泄露.gitlab-ci.yml文件。实际影响要严重得多,因为它能触发 GitLab 的“配置解析链式加载”机制。当攻击者成功获取到原始 YAML 内容后,可以进一步利用其中的include关键字,递归拉取外部配置片段。例如,一个典型的生产配置可能是:

include: - local: '/templates/deploy.yml' - remote: 'https://internal-configs.corp/templates/secrets.yml' variables: AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID

此时,攻击者不仅能拿到主配置,还能通过解析include字段,向 GitLab 发起新的 API 请求去拉取/templates/deploy.yml(属于同一项目内的文件,权限校验同样失效),甚至能构造恶意请求,让 GitLab 的remote加载器去请求内网地址(如http://10.1.2.3:8080/secrets.yml),从而实现服务端请求伪造(SSRF)。我们实测发现,在未启用allow_local_includeallow_remote_include严格限制的旧版本 GitLab 中,这种 SSRF 可以穿透 DMZ 区域,直接探测到核心数据库服务器的端口状态。这才是 CVE-2025-6948 最危险的地方:它不是一个孤立的权限绕过,而是一把能撬动整个 CI/CD 配置生态的万能钥匙。

3. 临时缓解方案:不升级也能守住防线的四道“物理隔离墙”

3.1 第一道墙:立即禁用所有 Reporter 用户的 API 访问能力(最有效)

这是我们在客户现场 2 小时内落地的首选方案。GitLab 并没有提供“禁止 Reporter 使用 API”的全局开关,但可以通过数据库直连方式,批量修改用户权限。核心思路是:将所有Reporter角色用户的access_level字段,在project_members表中临时降级为Guest(值为 10),因为Guest权限默认无法访问任何 Pipeline 相关 API。操作步骤如下(需 GitLab 管理员权限):

  1. 进入 GitLab Rails 控制台:
    sudo gitlab-rails console -e production
  2. 执行降级脚本(此操作仅影响 Reporter,不影响 Maintainer 和 Developer):
    # 获取所有 Reporter 角色的 project_members 记录 reporter_members = ProjectMember.where(access_level: 20) puts "即将降级 #{reporter_members.count} 个 Reporter 用户" # 批量更新为 Guest(access_level = 10) reporter_members.in_batches(of: 100).update_all(access_level: 10) # 验证更新结果 puts "更新后 Reporter 数量:#{ProjectMember.where(access_level: 20).count}"

提示:此操作是原子性更新,不会中断 GitLab 服务。但需注意,降级后 Reporter 将无法在 Web UI 中查看 Pipeline 列表,这是可接受的业务妥协——毕竟安全优先级高于部分只读功能。

3.2 第二道墙:用 Nginx 重写规则封堵高危 API 路径(零代码改动)

如果你无法直接操作数据库,Nginx 是最快速的兜底方案。GitLab 社区版默认使用 Nginx 作为反向代理,我们可以在gitlab.conflocation /api/v4/块中插入精准拦截规则:

# 在 location /api/v4/ {} 内添加 if ($request_uri ~ "^/api/v4/projects/[0-9]+/pipelines/[0-9]+/jobs/[0-9]+/trace$") { return 403; } if ($http_x_gitlab_feature_flag ~ "ci_pipeline_config_access_control=disabled") { return 403; }

这两条规则分别从 URI 路径和请求头两个维度进行拦截。重点在于正则表达式的严谨性:[0-9]+确保只匹配数字 ID,避免误杀/api/v4/projects/my-group/my-project/...这类新式路径;而对X-GitLab-Feature-Flag的匹配使用~(区分大小写)而非~*,因为 GitLab 内部解析该 Header 时是严格大小写的。我们曾测试过,如果写成~*,会导致所有带feature-flag的合法请求(如某些监控探针)也被误拦,引发告警风暴。

3.3 第三道墙:强制启用 CI 配置文件的“本地包含白名单”(治本之策)

GitLab 16.10+ 版本提供了include加载的安全控制开关,但默认是关闭的。你需要在/etc/gitlab/gitlab.rb中显式开启并配置白名单:

# 启用本地 include 安全检查 gitlab_rails['ci_include_local_enabled'] = true # 限定只允许 include 项目根目录下的 templates/ 子目录 gitlab_rails['ci_include_local_path_whitelist'] = ['templates/**/*'] # 禁用远程 include(彻底杜绝 SSRF) gitlab_rails['ci_include_remote_enabled'] = false

然后执行sudo gitlab-ctl reconfigure使配置生效。这个配置的精妙之处在于templates/**/*的 glob 模式:**表示递归匹配任意层级子目录,*匹配任意文件名,这样既允许templates/deploy/staging.yml这类深层路径,又禁止了../secrets.yml这种路径遍历。我们曾遇到客户误配为templates/*,结果导致templates/deploy/deploy.yml无法加载,CI 流水线全部失败——这就是为什么必须用**

3.4 第四道墙:用 Git Hooks 在代码提交层拦截硬编码密钥(主动防御)

所有被动防护都只能减少损失,真正的主动防御是在密钥泄露发生前就把它扼杀在摇篮里。我们在所有客户仓库的.gitlab-ci.yml中强制加入预提交检查(Pre-commit Hook),使用开源工具gitleaks

stages: - validate validate-secrets: stage: validate image: zricethezav/gitleaks:latest script: - gitleaks detect --source=. --no-git --verbose --config gitleaks.toml || exit 1 allow_failure: false

关键在gitleaks.toml配置文件中,我们自定义了针对 GitLab CI 变量的高精度规则:

[[rules]] description = "GitLab CI Variable Hardcoded Secret" regex = '''(?i)\b(AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|DOCKER_AUTH_CONFIG|KUBECONFIG)\s*:\s*["']([^"']+)["']''' tags = ["ci", "secret"]

这个正则表达式专门捕获 YAML 中形如AWS_ACCESS_KEY_ID: "xxx"的硬编码模式,且忽略大小写。它比默认规则更准,因为默认规则会误报AWS_REGION: us-east-1这类非密钥字段。上线后,某客户在一次日常提交中,gitleaks拦截了开发人员误提交的测试环境数据库密码,避免了一次潜在的泄露事件。

4. 根本解决方案:升级不是“一键操作”,而是四步验证闭环

4.1 升级前必做:用 GitLab 自带的gitlab-ctl check做兼容性快筛

GitLab 官方文档建议“直接升级到 17.3.0”,但现实是,很多企业环境存在定制化集成(如 LDAP 同步脚本、自定义 Runner 镜像、SAML IdP 配置)。盲目升级可能导致服务不可用。我们总结出一套四步快筛法,耗时不到 15 分钟:

  1. 检查数据库迁移状态

    sudo gitlab-ctl pg-upgrade-status # 输出应为 "No pending upgrades",否则需先完成 PG 升级
  2. 验证 Redis 连接健康度

    sudo gitlab-ctl redis-cli ping # 必须返回 "PONG",否则升级过程中 Redis 会成为单点故障
  3. 扫描自定义配置冲突

    sudo gitlab-ctl reconfigure --dry-run 2>&1 | grep -E "(error|conflict)" # 重点关注 "conflict" 关键字,它会指出 gitlab.rb 中哪些行与新版不兼容
  4. 模拟启动流程

    sudo gitlab-ctl start && sudo gitlab-ctl status | grep "down" # 确保所有组件(unicorn, sidekiq, nginx)都显示 "run"

注意:--dry-run参数在 GitLab 16.8+ 版本才支持,低于此版本需改用sudo gitlab-ctl show-config手动比对。

4.2 升级中关键:备份策略必须包含“Runner 注册令牌”这个隐形资产

几乎所有升级指南都强调备份/var/opt/gitlab和 PostgreSQL 数据库,却极少提及Runner的注册令牌(Registration Token)。这个令牌存储在/var/opt/gitlab/gitlab-rails/etc/gitlab.yml中的runner_registration_token字段,它决定了 Runner 是否能重新连接到 GitLab。一旦升级后令牌变更,所有离线 Runner 将永久失联,CI 流水线直接瘫痪。我们的标准操作是:

  1. 升级前导出当前令牌:

    sudo gitlab-rake gitlab:backup:create SKIP=db,uploads # 此命令生成的 backup.tar 中包含 gitlab.yml,但更直接的方式是: sudo cat /var/opt/gitlab/gitlab-rails/etc/gitlab.yml | grep runner_registration_token
  2. 将令牌值记录到安全笔记,并在升级后第一时间执行:

    sudo gitlab-ctl restart sudo gitlab-rake gitlab:check SANITIZE=true # 然后手动更新所有 Runner 的 config.toml 中的 token 字段

我们曾在一个金融客户现场,因忽略此步骤,导致 37 台专用构建机全部掉线,恢复耗时 4 小时——这比升级本身还长。

4.3 升级后必验:用真实 CI 任务验证“漏洞路径是否真正失效”

升级完成不等于风险解除。必须用生产环境的真实数据验证。我们设计了一个最小化验证用例:

  1. 创建一个测试项目security-test-project
  2. 在该项目中创建一个含敏感变量的.gitlab-ci.yml
    variables: DB_PASSWORD: "prod-secret-123" test-job: script: echo "test"
  3. 用 Reporter 用户 Token,尝试访问:
    curl -H "PRIVATE-TOKEN: reporter_token" \ "https://gitlab.example.com/api/v4/projects/$(get_project_id)/pipelines/$(get_pipeline_id)/jobs/$(get_job_id)/trace"

    提示:get_project_id等函数可用curl -s "https://gitlab.example.com/api/v4/projects?search=security-test-project" | jq '.[0].id'替代。

预期结果必须是403 Forbidden,且响应体中不包含任何 YAML 内容。如果返回200404(表示路径存在但无权限),说明漏洞仍未修复,需立即回滚。

4.4 升级后加固:启用 GitLab 17.3+ 的“Pipeline Config Audit Log”(长期防御)

GitLab 17.3 引入了全新的审计日志类别pipeline_config_access,它会记录每一次对.gitlab-ci.yml的读取行为,包括请求者 IP、User ID、项目 ID 和访问时间。启用方法很简单,在gitlab.rb中添加:

gitlab_rails['audit_events_enabled'] = true gitlab_rails['audit_events_for_pipeline_config_access'] = true

然后sudo gitlab-ctl reconfigure。这个日志的价值在于:它让你能回答一个过去无法回答的问题——“谁在什么时候,以什么身份,读取了哪个项目的 CI 配置?” 我们帮一家电商客户启用后,一周内就发现了一个异常模式:某个运维账号在凌晨 2 点频繁访问 12 个不同项目的配置文件,经核查是其个人脚本在做自动化巡检,但未按规范申请更高权限。这证明,好的安全不是靠堵,而是靠“看见”。

5. 经验复盘:踩过的三个坑,比解决方案本身更有价值

第一个坑是“过度信任官方补丁说明”。GitLab 在 CVE 公告中写道:“升级至 17.3.0 即可修复”,但我们发现,如果客户使用的是 Omnibus 包安装的 GitLab,且操作系统是 CentOS 7,17.3.0 的 RPM 包依赖glibc >= 2.28,而 CentOS 7 默认是glibc 2.17。强行安装会导致gitlab-ctl命令直接崩溃。解决方案不是升级系统(风险太大),而是改用 Docker 方式部署 GitLab 17.3.0,用docker run -d --name gitlab -p 443:443 -p 80:80 -p 22:22 -v /srv/gitlab/config:/etc/gitlab -v /srv/gitlab/logs:/var/log/gitlab -v /srv/gitlab/data:/var/opt/gitlab sameersbn/gitlab:17.3.0。这个方案绕开了系统依赖,且容器化部署本身也提升了隔离性。

第二个坑是“忽略 Runner 的缓存污染”。GitLab 升级后,Runner 的本地镜像缓存(Docker layer cache)可能仍保留着旧版 GitLab 的认证逻辑。我们遇到过一次诡异现象:升级后漏洞路径返回 403,但同一个 Reporter 用户用gitlab-runner exec docker test-job命令本地运行时,却能成功读取配置。根源在于 Runner 的exec模式绕过了 GitLab 的 API 权限校验,直接读取本地仓库文件。解决方案是升级后立即清理 Runner 缓存:sudo gitlab-runner unregister --all-runners && sudo gitlab-runner register --non-interactive ...,并强制所有 Runner 重新拉取最新版gitlab/gitlab-runner:alpine-v17.3.0镜像。

第三个坑最隐蔽:“CI 变量作用域继承导致的权限错觉”。GitLab 允许在 Group 级别设置 CI 变量,并向下继承到 Project。我们有个客户,把PROD_DB_PASSWORD设在了顶级 Group,所有子项目自动继承。升级后,他们以为漏洞已修复,却忽略了:Reporter用户虽然不能通过 API 读取.gitlab-ci.yml,但只要他能触发一个 Job,该 Job 的执行环境里依然会注入PROD_DB_PASSWORD变量。这意味着,如果 Job 脚本里有echo $PROD_DB_PASSWORD,敏感信息就会直接打印在 Job 日志里。最终解决方案是:在 Group 级变量设置中,勾选 “Protected” 和 “Mask variable” 选项,并将变量作用域限制为production环境,确保只有production环境的 Job 才能访问。

最后再分享一个小技巧:GitLab 的漏洞修复往往伴随着性能调整。CVE-2025-6948 的补丁在app/controllers/api/v4/pipelines_controller.rb中新增了authorize_pipeline_config!方法,该方法会触发一次额外的数据库查询来校验ProjectFile权限。如果你的 GitLab 实例每秒处理超过 500 个 API 请求,建议在 PostgreSQL 中为project_files表的project_id字段创建索引:

CREATE INDEX CONCURRENTLY idx_project_files_project_id ON project_files (project_id);

我们实测发现,加索引后,高并发场景下/pipelines/:id/jobs/:job_id/trace接口的 P95 延迟从 1200ms 降至 85ms。安全和性能,从来就不是单选题。

http://www.jsqmd.com/news/885731/

相关文章:

  • Linux 调度域的 flags 标志:负载均衡的策略控制
  • 2026 合肥家具工厂直营店性价比排行:3 家本地人公认的省钱好店 - 资讯快报
  • 【checkBox】
  • Linux服务器入侵排查实战:时间线、权限链与行为流三要素
  • 鸿蒙PC:从一个普通 Electron 项目到鸿蒙可运行项目:vmd-master 适配实战全记录
  • Claude投资回收期正在缩短!2024Q2最新基准线曝光:SaaS团队平均3.8个月,但92%企业算错了这1个折现因子
  • 2026年1688开户代运营优选:衡水企信网络科技有限公司, 全国商家靠谱电商合作伙伴 - GrowthUME
  • 2026闭眼入!5款一键生成论文工具亲测,摆脱无效加班,初稿质量效率翻倍
  • Windows 11 LTSC系统安装微软商店的终极解决方案:告别应用荒的完整指南
  • E7Helper实战指南:5个核心技巧快速掌握第七史诗自动化助手
  • Unity开源项目版本兼容性问题诊断与跨版本适配指南
  • OpenSSH密钥交换漏洞CVE-2025-26465/26466纵深防御指南
  • 全域无死角监测,无感技术填补矿山安防空白
  • 20244321李梓睿 2025-2026-2 《Python程序设计》实验四报告
  • DIY迈克尔逊干涉仪:用光学原理实现微米级振动测量
  • 你的Creo‘未响应’,可能只是被Windows‘坑’了!深入xtop.exe与系统兼容性的那些事儿
  • 浏览器下载太慢?用Motrix扩展实现3倍下载加速
  • 保姆级教程:从零用Playwright+Pytest写一个带截图和Allure报告的百度搜索测试
  • AI教材写作必备!低查重AI工具助力,轻松编写优质教材!
  • 户外直播家用备用随身 WiFi 实测:2026 十大公认优质品牌机型盘点 - 资讯快报
  • rimage_gui:开源免费的批量图片压缩神器,视觉无损释放存储空间!
  • 2026广州越秀注册公司怎么选?5家本地老牌财税机构实测推荐(创业避坑干货) - 资讯快报
  • OPD 成熟度模型:评估你的部门离 AI 原生还有多远
  • 越权漏洞实战图谱:水平、垂直、目录与SQL跨库越权详解
  • 鸿蒙electron框架PC适配:ExifCleaner 适配鸿蒙全过程:一次从“能启动”到“能处理文件”的完整复盘
  • WaveTools深度解析:鸣潮游戏性能调优与数据管理技术实现
  • 块坐标下降(BCD)优化LLM训练:降低内存与成本
  • 2026年度深圳市训力券形式审查要点
  • 树莓派Zero离线语音识别实战:硬件配置、软件方案与性能优化
  • Topit终极指南:300%效率提升的macOS窗口置顶革命