Skip to content

GitLab GraphQL CVE-2026-19478 / CVE-2026-19650 critical patch

Summary

On August 17, 2026, GitLab released 19.2.4, 19.1.6, 19.0.8, and 18.11.11, fixing two GraphQL vulnerabilities in self-managed GitLab CE/EE:

  • CVE-2026-19478 (CVSS 9.4, Critical, CWE-94 code injection): a remote, unauthenticated user can modify or delete public projects and user data through a GraphQL input, via the rolling-deploy @gl_introduced(version:) version-filtering path.
  • CVE-2026-19650 (CVSS 7.1, High, CWE-352 CSRF): improper request validation in GraphQL multiplex query handling lets an unauthenticated user execute state-changing mutations via GET requests, breaking GitLab's read-only GET protection.

Only self-managed instances are affected; GitLab.com and GitLab Dedicated were already patched. There is no documented configuration workaround — upgrading to the matching patched branch is the only remediation. As of the last scan, neither CVE is in the CISA KEV catalog and no public exploitation infrastructure or actor linkage is reported; both were reported through GitLab's HackerOne bug bounty program.

Tags

Why this matters

  • CVE-2026-19478 is unauthenticated remote code execution-adjacent: a crafted GraphQL field name becomes a method invocation on a server-side object. Public projects are queryable without a session and the directive is processed before field-level authorization, so the write path needs no credentials.
  • The flaw lives in rolling-deploy compatibility code (FutureFieldFilter / FutureFieldFallback), not in a user-facing API surface, which makes it unlikely to show up in casual threat modeling of the GraphQL API.
  • CVE-2026-19650 converts a version-validation mismatch into a CSRF primitive: the query is validated from one document but executed from another, so a GET request that passes the "reads only" check can still execute a mutation in the victim's browser session.
  • The affected range is broad: self-managed CE/EE from 18.2 through 19.2.3 on all four maintained lines.

CVE-2026-19478 — unauthenticated GraphQL code injection

GitLab ships a @gl_introduced(version:) GraphQL directive so that during a rolling deploy, a client can request fields that only exist on newer instances without triggering a schema error. FutureFieldFilter strips any node whose version is ahead of the running instance and records contain_future_fields; IntroducedTracer restores the original document at execution time and sets context[:contain_future_fields] = true so the stripped fields resolve to null instead of erroring.

That flag reaches the vulnerable code. During execution, FutureFieldFallback#get_field intercepts every field lookup, and when the flag is set and the requested field does not exist in the schema, it synthesizes a field on the fly from the attacker's own field name. The synthesized GraphQL::Schema::Field carries no resolver, and in graphql-ruby a field without a resolver resolves by calling the method of the same name on the underlying object. Because the field name comes straight from the query document, marking any name with @gl_introduced(version: "99.0") turns it into a method invocation against whatever object sits at that position in the graph — which is how a read-only query reaches state-changing methods on a model and reaches remote modification and deletion of public projects and user data.

The vulnerable code is lib/gitlab/graphql/version_filter/future_field_fallback.rb (v19.2.2-ee). The future_field? gate only checks the context flag, that the field is absent from the schema, and that the name does not start with __ — there is no check on what the name actually is.

The patch (commit e283c6ad, "Prevent calling object method when resolving fallback field") does not sanitize or restrict the field name, which stays fully attacker-controlled. It removes the dispatch instead: the two lines that declared the field's type and fallback value are replaced by a single resolver_class pinned to a new NilResolver (app/graphql/resolvers/nil_resolver.rb) whose resolve returns nil unconditionally. The fallback keeps its original purpose — a missing future field still resolves to null during a rolling deploy — while the path from a query string to an arbitrary method call on a server-side object is gone.

CVE-2026-19650 — multiplexed-query document swap enables GET mutations

IntroducedTracer backs the @gl_introduced directive by holding onto the pre-filter query document, so execution can fall back to null for fields the running version does not have. It kept that state in two plain instance variables on the tracer: @original_query_document and @contain_future_fields.

One instance variable holds one document, but a GraphQL multiplex request carries several queries through the same tracer. parse runs once per query in the batch, so each call overwrites the document stored by the previous one. By the time execute_query runs for any individual query, the stored document belongs to whichever query in the batch parsed last, and it is force-installed onto the executing query via instance_variable_set(:@document, …) followed by prepare_ast.

The consequence: the document a query is validated as is not the document it executes as. That breaks the assumption behind GitLab's GET protection — GraphQL over GET is allowed for read-only operations and mutations are rejected, but the rejection is decided from the parsed query while execution proceeds from the swapped-in document. A batch whose first query is a benign read and whose second carries a mutation marked with @gl_introduced passes the check and executes the mutation, yielding a cross-site request forgery primitive over GET — matching the UI:R in the vector, since the victim's browser must be induced to issue the request. The @contain_future_fields flag has the same problem: one query in a batch containing a future field switches the substitution on for all of them.

The patch (commit d2ea4b97, "Prevent query swapping with multiplexed queries and gl-introduced") introduces a per-tracer hash: parse writes each query's original document under the filtered document it produced, and execute_query retrieves the entry for query.document rather than reading a shared slot. The substitution is also wrapped in a conditional, so a query with no matching entry falls through to normal execution.

Affected and patched versions

Product Affected versions Patched version
GitLab CE/EE 18.2 – 18.11.10 18.11.11
GitLab CE/EE 19.0 – 19.0.7 19.0.8
GitLab CE/EE 19.1 – 19.1.5 19.1.6
GitLab CE/EE 19.2 – 19.2.3 19.2.4

GitLab.com and GitLab Dedicated are already patched; no action is needed for hosted instances.

Defender priorities

  1. Upgrade the self-managed fleet to 18.11.11 / 19.0.8 / 19.1.6 / 19.2.4 matching each branch. This is the only remediation; there is no documented configuration workaround.
  2. Validate exposure while you patch. Confirm which instances expose GraphQL to the internet, whether public projects are enabled, and whether a WAF or reverse proxy sits in front of /api/graphql.
  3. Treat unauthenticated writes to public projects as a compromise signal. Search audit events and application logs for out-of-band project modification or deletion on public projects, especially from sessions with no authenticated principal, around any suspicious GraphQL activity.
  4. Watch for the KEV question to change. Neither CVE was in the CISA KEV catalog as of the last scan; both are unauthenticated-reachable and internet-exposed GitLab instances are large in number, so exploit availability is plausible even without a confirmed KEV entry.

Assessment limits

  • No public exploitation, PoC release, or actor linkage has been reported; both CVEs came through the GitLab HackerOne program (reported by hiimguardian and kreep respectively).
  • CISA KEV status: not listed as of the last scan. Re-check on each scan.
  • OX Security published the technical analysis; the vulnerability mechanics above are as described in that analysis.

Sources