Threat Intel GitLab Active Exploit

One GraphQL directive deletes any public GitLab repo

August 24, 2026·7 min read·Chris Boker, Founder, CVEasy AI
A dark GitLab server panel in the middle carrying a green top bar and three public project tiles, a rust attacker cluster on the left sending an unauthenticated POST arrow into a mint GraphQL query card that contains a bold gold @gl_introduced directive badge and a small attacker-supplied field row, a green arrow leaving the query card and bending through a dashed mint authorization gate on the front of the panel by way of a gold bypass hook, a rust dispatch arrow landing on one of the three project tiles which is struck by a gold X marking a deleted repository, and a bottom row of six mint version tiles marked 19.2.4, 19.1.6, 19.0.8, and 18.11.11 with the earlier tiles greyed out

GitLab pushed an out-of-band patch on Sunday, August 17. Their normal cadence is twice a month; a Sunday emergency drop means something got past the code review that public GraphQL access is supposed to have. CVE-2026-19478 landed at CVSS 9.4, CWE-94, and by August 20 Help Net Security and The Hacker News were tracking active exploitation against internet-facing self-managed instances. watchTowr had it reproducing from the advisory and the patch diff within minutes.

The attack is a single unauthenticated POST to /api/graphql. No login, no session. The body carries an ordinary-looking GraphQL document with one attacker-supplied field marked @gl_introduced(version: "99.0"). What comes back is not a query result. What comes back is a project you no longer own.

The directive that was there to help a rolling deploy

GitLab installs get upgraded in place, and during a rolling upgrade the API layer runs a mix of old and new schemas depending on which pod answered the load balancer. A client cannot depend on any given field existing across two consecutive requests, so GitLab ships @gl_introduced(version: "…") as a hint: this field only lives on newer instances, resolve it to null if you are behind, do not throw a parse error and abort the whole query.

Three components carry that. FutureFieldFilter strips any node whose declared version: is ahead of the running instance and flips context[:contain_future_fields] = true. IntroducedTracer restores the original document at execution time. FutureFieldFallback#get_field runs on every field lookup, notices the flag, and when the requested field does not exist on the current schema, synthesizes a GraphQL::Schema::Field on the fly out of the attacker's own field name. That synthesized field has no resolver.

That is the bug. In graphql-ruby, a field with no resolver defaults to calling a method of the same name on the underlying object.

What a synthesized field with no resolver actually does

The graph a public GitLab project exposes has concrete server-side Ruby objects at every position. Ask for project(fullPath: "group/repo") and the resolver hands back a Project; ask for project.namespace and you get a Namespace. Anywhere a field would normally live, an object is already there, waiting to be asked for one of the fields the schema whitelists.

FutureFieldFallback#get_field removes the whitelist. Once the flag says future fields are in play and the requested field is not on the schema, the fallback stops asking the schema and starts asking the object directly. Mark my_field @gl_introduced(version: "99.0") at the Project node and the executor calls project.my_field. Point that at a real method on the Project model and the executor calls that method.

Project has methods that change state. So do the models behind merge requests, project members, and every other node the graph exposes. The directive was reviewed as a schema-compat helper; the authorization layer that would have refused a state-changing call is scoped to fields the schema declares, not to methods the fallback synthesizes. Field-level authorized? hooks never fire on this code path, and neither does the policy check that gates destroy services. The synthesized field slips underneath both.

That is why the impact reads the way it does across the reporting at SecurityWeek, Dark Reading, and the CyCognito emerging-threat writeup: an unauthenticated attacker can delete public repositories, forge merge records against them, or ban maintainers, in one HTTP request, with no user interaction.

What actually broke: A directive built to make schema evolution safe during rolling deploys was wired into the field lookup path in a way that returns a resolver-less field for any name the attacker types. graphql-ruby then dispatches the name as a method against the object at that graph position, and no authorization hook runs on that dispatch.

Why a base-score queue drops it below other 9.4s

The finding is a 9.4 in CVSS. So is a January stack overflow in an internal ERP nobody exposes. A queue sorted on base score alone puts them next to each other and buries the one actively being scanned for. Two things the base score cannot see here: whether the instance is reachable from the internet, and whether it runs one of the specific vulnerable minors. Both are answerable, and both change the priority by an order of magnitude.

Per GitLab's critical patch announcement, the vulnerable range covers 18.2 through 18.11.10, 19.0 through 19.0.7, 19.1 through 19.1.5, and 19.2 through 19.2.3. Many self-managed shops sit on the previous minor by policy to avoid brand-new bugs. That policy put them squarely in the exposed set.

How TRIS ranks this on your GitLab estate

Two self-managed GitLab servers, both flagged with CVE-2026-19478 at CVSS 9.4, can carry very different exposures. Server A serves a public developer portal on the open internet with SSO optional. Server B runs on a segmented build network reachable only from CI runners in a private subnet, and is already on 19.2.4. CVSS treats them the same. TRIS, the Threat and Risk Intelligence Scoring engine inside CVEasy AI, decides this finding across four layers instead.

Exploitation. watchTowr honeypot captures inside two days of disclosure, opportunistic scanning corroborated across multiple trackers, and an emergency out-of-band vendor release. This layer sits at the top of its range: real active abuse against live instances, not a proof of concept in a researcher's blog post.

Exposure. Is the instance internet-reachable, and does it host any project marked public? Public projects are the ingress the directive rides on, because the read path executes before a session is required. An instance behind an identity-aware proxy that strips unauthenticated hits before /api/graphql sees them scores far lower on this layer, even without the patch.

Blast radius. What lives on the instance? A community mirror with disposable public forks is one story. A production instance hosting release tags used by downstream CI, deploy keys for infrastructure repos, or signed artifacts for customer distribution is a very different one. TRIS carries the value graph forward so the same CVE scores by what the deletable resources actually protect.

Contained. Was the instance already on 19.2.4, 19.1.6, 19.0.8, or 18.11.11 when the CVE was assigned? Is the front door filtered by an authenticating proxy? Is /api/graphql blocked at the load balancer for anonymous clients? Contained lowers the tier without removing the finding, because the underlying primitive is still one config drift from live.

An internet-facing GitLab still on 19.2.3 hosting public projects sits at the top ACT band. The same CVE on a segmented, patched, proxy-fronted instance sits far lower. Same score in CVSS, opposite ends of the queue in TRIS, and the queue is right.

Patch, hunt, harden the front door

Patch. Upgrade to 19.2.4, 19.1.6, 19.0.8, or 18.11.11, depending on your minor. Both CE and EE are affected. GitLab.com and GitLab Dedicated were pre-patched, so only self-managed installs need action. There is no runtime workaround that leaves the code path open while blocking the primitive.

Hunt. Grep GitLab and reverse-proxy access logs for @gl_introduced inside the raw body of unauthenticated POST /api/graphql requests. Legitimate clients rarely construct that directive by hand outside a rolling-deploy window. Also watch GET /api/graphql?query=... for mutations riding through the multiplex handler, because the same August 17 release fixed CVE-2026-19650, a CSRF that lets mutations arrive over GET, per OX Security's dual-CVE writeup.

Interim mitigation. If you cannot patch immediately, restrict unauthenticated access to /api/graphql at the reverse proxy, or temporarily set public projects to internal until the upgrade is in. Both are blunt instruments; both close the door.

Rotate what the primitive could reach. If your instance was internet-facing and unpatched after August 17, treat every deploy key, personal access token, and CI/CD variable on affected projects as potentially tampered. Recreate protected branches from a known-good mirror, verify tag signatures, and rebuild any release built from public repos in the exposure window. The bug hands out object.method on the graph; assume it was used.

Do not wait for confirmed abuse to act: The exploitation window opened on August 17 and the honeypot captures landed on August 20. If your GitLab was reachable and unpatched during that window, patch first, then hunt, then rotate. Confirming or denying tampering from access logs alone is unreliable, because the primitive resolves inside a routine-looking GraphQL request body.

Where CVEasy AI surfaces this

CVEasy AI is the number one local-first Continuous Threat Exposure Management (CTEM) platform. It ingests the real sources on release (GitLab's critical patch notes, the CVE record, watchTowr honeypot telemetry) and runs TRIS against the inventory it holds on your hardware: which GitLab instances are on what minor, whether they face the internet, whether they host any project marked public, whether an authenticating proxy fronts /api/graphql. It answers the question that decides your week: which of my GitLab servers sits in the top ACT band right now, what would be lost if the primitive fired, and what do I patch or block first. Your inventory, your access logs, and your rotation plan stay on your hardware while TRIS does the arithmetic. That is what local-first CTEM means, and CVE-2026-19478 is the class of finding it exists to answer.

Sources: Help Net Security, The Hacker News, SecurityWeek, Dark Reading, GitLab patch forum, CVE Record, OX Security, CyCognito, SOC Prime

Your CVSS queue treats a public GitLab and a segmented one identically

CVEasy AI's TRIS engine scores CVE-2026-19478 against your real exposure: which instances face the internet, which run a vulnerable minor, whether a proxy fronts /api/graphql. Local-first, on your hardware.

Related Reading