← Back to Feed CACHED · 2026-10-07 20:24:37 · CACHE_KEY CVE-2026-19650
CVE-2026-19650 · CWE-352 · Disclosed 2026-08-17

GitLab CE/EE GraphQL Multiplex IntroducedTracer CSRF Allowing Mutation Execution via GET

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

Like leaving a loaded gun with a hair trigger — anyone who bumps the link fires it with the victim's own hands

CVE-2026-19650 is a cross-site request forgery in GitLab CE/EE's GraphQL multiplex query handler affecting all self-managed versions from 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. The IntroducedTracer component stores the parsed query document in a shared instance variable (@original_query_document) that each query in a multiplex batch overwrites. An attacker crafts a multiplex GET request where slot 0 is a benign read-only query (which passes GET-method validation) and slot 1 is a state-changing mutation tagged with @gl_introduced. By the time execution runs, the stored document belongs to the last-parsed query — the mutation — so slot 0's validated-safe context executes slot 1's mutation. The victim only needs to click a link or load an attacker-controlled page while authenticated to their GitLab instance. Successful exploitation lets the attacker create, update, or delete repositories, issues, merge requests, and other GitLab resources using the victim's session and permissions.

GitLab's vendor severity of HIGH / 7.1 is accurate and well-calibrated. The CVSS vector correctly captures UI:R (user interaction required), PR:N (attacker needs no GitLab account), I:H (high integrity impact from arbitrary mutations), C:N (no data exfiltration path from this bug alone), and A:L (limited availability impact via resource deletion). This is not a zero-click flaw — the companion CVE-2026-19478 (CVSS 9.4) is the one that's unauthenticated and zero-interaction. CVE-2026-19650 is a CSRF that rides the same underlying IntroducedTracer race condition but requires social engineering to land. The CSRF friction is already baked into the 7.1 score, and the supply-chain role of GitLab (CI/CD, source code, pipeline definitions) keeps the floor at HIGH. GitLab.com and GitLab Dedicated are already patched — only self-managed deployments remain exposed.

"GitLab CSRF lets attackers run mutations as the victim — unchanged at HIGH due to supply-chain blast radius."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Craft multiplex GET payload

The attacker constructs a URL targeting the victim's GitLab instance at /-/graphql?_method=GET containing a multiplex batch. Slot 0 holds a benign query { currentUser { id } } that passes GET-method mutation rejection. Slot 1 holds the desired mutation (e.g., mutation { destroyProject(input: { id: "gid://gitlab/Project/42" }) { ... } }) decorated with @gl_introduced. No special tooling is required — a browser-renderable URL or <img> tag suffices. The poc_cve_2026_19650.py script in the dinosn/gitlab-cve-2026-19478-lab repo demonstrates this swap non-destructively.
Conditions required:
  • Attacker must know (or guess) the target GitLab instance hostname
  • Target instance must be running an unpatched self-managed GitLab CE/EE version
Where this breaks in practice:
  • Self-managed GitLab URLs are often internal-only, limiting attacker reachability
  • Instance version fingerprinting is needed to confirm exploitability
Detection/coverage: Qualys QID or Tenable plugin for GitLab version detection; the poc_cve_2026_19650.py safe probe confirms the swap behavior
STEP 02

Deliver the link to an authenticated victim

The attacker sends the crafted URL to a GitLab user via email, Slack, an MR comment on a public project, or any channel the victim trusts. The link can be disguised as a GitLab deep-link. Alternatively, the payload can be embedded as an invisible <img src="..."> on a page the victim is likely to visit. The victim must have an active, authenticated session (session cookie) with the target GitLab instance at the time they load the URL.
Conditions required:
  • Victim must be authenticated to the target GitLab instance
  • Victim must click the link or load the attacker-controlled page
  • Attacker must reach the victim through a social-engineering channel
Where this breaks in practice:
  • Modern email gateways may strip or warn on unfamiliar URLs
  • Security-aware users may not click unsolicited GitLab links
  • SameSite cookie defaults in modern browsers can block cross-origin cookie attachment, depending on GitLab's cookie configuration
Detection/coverage: Email gateway or web proxy logs showing outbound requests to internal GitLab URLs from external referrers; GraphQL audit logs showing GET-method mutations
STEP 03

Mutation executes under victim's session

When the victim's browser loads the crafted URL, GitLab's GraphQL multiplex handler processes both slots. The IntroducedTracer overwrites the stored query document with slot 1's mutation during parsing. Slot 0's execution context now runs the mutation from slot 1 using the victim's CSRF token and session cookie. The mutation executes with whatever permissions the victim holds — a project maintainer can delete repos; an admin can modify instance-level settings.
Conditions required:
  • GitLab instance is running a vulnerable version (pre-18.11.11 / 19.0.8 / 19.1.6 / 19.2.4)
  • The GraphQL endpoint accepts the multiplex GET with the query-swap payload
Where this breaks in practice:
  • GitLab's Rails CSRF token mechanism should block cross-origin POST mutations, but GET requests bypass this — the whole point of the bug
  • If the victim has a low-privilege Guest role, the blast radius is minimal
Detection/coverage: GitLab production logs: look for GraphQL multiplex GET requests containing mutation keywords; Sigma rules in cvemapping/cve-labs repo cover this pattern
STEP 04

Supply-chain or data-integrity impact realized

Depending on the victim's role, the attacker achieves: project deletion (Maintainer+), pipeline definition modification via .gitlab-ci.yml changes (Developer+), merge-request approval manipulation, or issue/snippet/wiki tampering. Against an admin target, instance-level mutations may be reachable — including user creation, runner registration, or integration token generation. In a CI/CD-centric deployment, modifying pipeline YAML is a supply-chain compromise vector: the next pipeline run executes attacker-controlled code.
Conditions required:
  • Victim has sufficient GitLab permissions for the target mutation
  • The targeted project/resource exists and is accessible to the victim
Where this breaks in practice:
  • Attacker must know specific project IDs or resource GIDs to target — blind exploitation is noisy and speculative
  • Destructive actions (project deletion) trigger GitLab notification emails, raising immediate visibility
Detection/coverage: GitLab audit events for project deletion, pipeline changes, and runner registration; SIEM correlation of GraphQL mutation events with GET method anomaly
03 · Compensating Control

1
HIGH 7.1→IGNORE 0.0
SEVERITY REDUCED
Patch to fixed GitLab versions immediately — Upgrade to 18.11.11, 19.0.8, 19.1.6, or 19.2.4. This is the definitive fix — the patch isolates IntroducedTracer state per multiplex slot, preventing the query-swap. Per noisgate remediation SLA for HIGH, deploy within 180 days. However, given the companion CVE-2026-19478 is actively exploited and shares the same patch, you should be patching for that reason within days regardless.
2
HIGH 7.1→LOW 2.5
SEVERITY REDUCED
Block GraphQL multiplex GET requests at the reverse proxy — Add a WAF or nginx rule to reject GET requests to /-/graphql that contain the _method parameter or multiplex batch syntax. Example: if ($request_method = GET) { ... return 403; } on the GraphQL endpoint. This breaks the specific attack vector without a GitLab upgrade. Test carefully — some GitLab UI features use GraphQL GET for read operations. Deploy within the noisgate mitigation SLA of 30 days for HIGH.
3
HIGH 7.1→LOW 2.0
SEVERITY REDUCED
Enforce SameSite=Strict on GitLab session cookies — Override GitLab's default SameSite=Lax to SameSite=Strict at the reverse proxy or Rails configuration level. This prevents the browser from attaching session cookies on any cross-site navigation, blocking the CSRF delivery entirely. Caution: this will break SSO/OAuth flows that rely on cross-site redirects — test in staging first. Deploy within 30 days.
4
HIGH 7.1→HIGH 6.5
Enable and monitor GraphQL audit logging — Enable GitLab's structured logging for GraphQL operations and alert on any mutation executed via GET method. This does not prevent exploitation but ensures detection within minutes. Correlate with Sigma rules from cvemapping/cve-labs for the multiplex swap pattern. Deploy immediately as a detection-only measure.
5
HIGH 7.1→MEDIUM 5.5
SEVERITY REDUCED
Restrict admin and maintainer accounts to hardware-token MFA — Ensure all GitLab accounts with Maintainer, Owner, or Admin roles use hardware-token (WebAuthn/FIDO2) MFA. While this does not prevent the CSRF itself, it limits the population of high-value targets whose sessions could be riding on a simple password-based login. The attacker's blast radius is bounded by the victim's permissions — reducing the number of highly-privileged sessions reduces supply-chain risk.
What doesn't work
  • Standard CSRF tokens — GitLab's Rails CSRF token mechanism protects POST/PUT/DELETE requests but explicitly allows GET requests through, which is the exact method this vulnerability exploits. The bug bypasses the mutation-over-GET rejection, not the CSRF token system.
  • Network-level IP allowlisting on the GitLab instance — The CSRF payload executes in the victim's browser, which already has legitimate network access to the GitLab instance. Restricting source IPs at the firewall does not help because the request originates from an authorized client.
  • Disabling public project access — The CSRF does not depend on public projects. It executes mutations under the victim's authenticated session against any resource the victim can access, including private projects.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNot observed. Not listed on CISA KEV. No GreyNoise tags, no Shadowserver scanning reports, no campaign attribution as of 2026-10-08. The sibling CVE-2026-19478 (CVSS 9.4) *is* being actively exploited — conflation risk is high.
Proof-of-conceptPublic, safe/detection-only. dinosn/gitlab-cve-2026-19478-lab on GitHub includes poc_cve_2026_19650.py — a non-destructive script that proves the multiplex query-swap on vulnerable instances (slot 0 returns slot 1's data). Derived from public fix commit d2ea4b971a98. No weaponized exploit with destructive mutations has been published. Not indexed on pocindex.io as of 2026-10-08.
EPSS0.00615 (~93rd percentile). Low probability of exploitation in the next 30 days, consistent with CSRF bugs that require social engineering.
CISA KEV statusNot listed. The companion CVE-2026-19478 may be added given active exploitation, but CVE-2026-19650 is not on the catalog.
CVSS vector interpretationCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L — Network-reachable, low complexity, no privileges required, user interaction required. No confidentiality impact; high integrity impact (mutations); low availability impact (deletions). The UI:R is the key differentiator from the companion 9.4 flaw.
Affected version rangesGitLab 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. Introduced in 18.2 with the @gl_introduced directive / IntroducedTracer feature.
Fixed versions18.11.11, 19.0.8, 19.1.6, 19.2.4. GitLab.com and GitLab Dedicated were patched server-side on 2026-08-17. Only self-managed instances require action.
Internet exposureCensys reports 86,000+ internet-facing GitLab self-managed instances (208K web properties). CyCognito estimates 20,000+ on standard web ports. ZoomEye returned 1.3M matching assets for app="GitLab". Attack surface for CSRF is any instance reachable by the victim's browser — including internal instances accessed via VPN/corporate network.
Disclosure date2026-08-17 (out-of-band patch release). Technical details embargoed for ~90 days (mid-November 2026).
Reporting researcherkreep and diablosec via GitLab's HackerOne bug bounty program. Bounty: $5,530.

Sources.

  1. GitLab Critical Patch Release 19.2.4 (vendor advisory)
  2. Ox Security — GitLab GraphQL CVEs: CVE-2026-19478 & CVE-2026-19650 (technical deep-dive)
  3. The Hacker News — Critical GitLab GraphQL Flaw Could Let Unauthenticated Attackers Delete Public Projects
  4. GBHackers — GitLab Patches Multiple Security Flaws in CE and EE With 19.2.4 Update
  5. dinosn/gitlab-cve-2026-19478-lab — Reproducible PoC lab (GitHub)
  6. cvemapping/cve-labs — Detection analytics for CVE-2026-19650 (GitHub)
  7. CyberNews — Thousands of vulnerable GitLab servers exposed online
  8. DEV Community — Two GitLab GraphQL Vulnerabilities analysis
05 · The Call

Final Verdict
= UNCHANGED to HIGH (7.1/10)

Why this verdict

  • CSRF friction is real but already priced in. The UI:R requirement means an attacker must socially engineer a GitLab user into clicking a link or loading a page. This is not trivial at scale, but the CVSS 7.1 already accounts for this — UI:R drops the score from what would otherwise be ~9.1. No further downgrade is warranted for a friction factor already in the vector.
  • Self-managed only, but surface is large. GitLab.com and Dedicated are patched, limiting exposure to self-managed instances. However, 86,000+ internet-facing and an unknown-but-larger number of internal-network self-managed instances remain in scope. The CSRF payload works regardless of whether the instance is internet-facing — it executes in the victim's browser context, which already has network access to the GitLab instance.
  • No active exploitation keeps this from upgrading. Unlike the companion CVE-2026-19478 (CVSS 9.4, actively exploited), CVE-2026-19650 has no KEV listing, no campaign reports, and an EPSS of 0.00615. The public PoC is detection-only, not weaponized. There is no urgency signal beyond the structural risk.
  • Role multiplier: GitLab is canonically a CI/CD and supply-chain component. GitLab self-managed instances serve as source code repositories, CI/CD pipeline engines, container registries, and artifact stores. This is explicitly in the high-value-role catalog (CI/CD + artifact stores). ≥90% of GitLab self-managed installs occupy this role by definition — it IS the CI/CD platform. If the CSRF victim is a Maintainer or Admin, the attacker can modify .gitlab-ci.yml pipeline definitions, inject malicious code into repositories, or delete projects — achieving supply-chain-scale blast radius. This floors the verdict at HIGH regardless of CSRF friction.
  • SameSite cookie behavior is a partial brake but not a full stop. GitLab sets SameSite=Lax on session cookies by default, which blocks cross-site POST but *allows* cross-site GET requests — which is exactly the vector this CVE exploits. The CSRF protection that should stop mutations over GET is the exact mechanism this bug bypasses. SameSite=Lax does not mitigate this flaw.

Why not higher?

This is not CRITICAL because the attack requires user interaction — a human must click a link or load a page while authenticated. The attacker cannot fire this at scale against arbitrary GitLab instances without a social-engineering delivery mechanism for each target. The companion CVE-2026-19478 is CRITICAL precisely because it needs no interaction. Additionally, there is no active exploitation, no KEV listing, and the EPSS score is in the low single digits. The structural ceiling for a CSRF with UI:R and C:N is firmly in the HIGH band.

Why not lower?

Downgrading below HIGH would require ignoring that GitLab is canonically a supply-chain-critical platform where successful exploitation against a privileged user yields pipeline injection, repository tampering, and project destruction. The CSRF friction (social engineering) is already captured in the 7.1 score via UI:R. The public PoC demonstrates the swap mechanism works reliably — the only gap is weaponization with destructive mutations, which is trivial to add. The 86,000+ internet-facing instances and the browser-context attack model (works against internal instances too) ensure the reachable population is substantial.

06 · Verification

Crowdsourced verification payload.

Run this script from any workstation that can reach the target GitLab instance. It sends a safe, non-destructive multiplex GraphQL GET request to detect the IntroducedTracer query-swap behavior. No authentication or elevated privileges required. Invoke with: python3 check_cve_2026_19650.py https://gitlab.example.com

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
"""CVE-2026-19650 detection — safe, non-destructive multiplex query-swap check.
Usage: python3 check_cve_2026_19650.py <GITLAB_BASE_URL>
Exit codes: 1 = VULNERABLE, 0 = PATCHED, 2 = UNKNOWN
"""
import sys
import json
import urllib.request
import urllib.parse
import ssl

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 check_cve_2026_19650.py <GITLAB_BASE_URL>")
        sys.exit(2)

    base_url = sys.argv[1].rstrip("/")

    # Two safe read-only queries with distinct return shapes.
    # On vulnerable instances, slot 0 returns slot 1's data (query-swap).
    query_a = '{"query": "query { currentUser { __typename } }"}'
    query_b = '{"query": "query { metadata { version } }"}'
    multiplex_payload = f"[{query_a}, {query_b}]"

    graphql_url = f"{base_url}/-/graphql"
    params = urllib.parse.urlencode({"_method": "GET", "operations": multiplex_payload})
    full_url = f"{graphql_url}?{params}"

    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    req = urllib.request.Request(full_url, method="GET")
    req.add_header("Accept", "application/json")
    req.add_header("User-Agent", "noisgate-cve-2026-19650-check/1.0")

    try:
        with urllib.request.urlopen(req, timeout=15, context=ctx) as resp:
            body = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        if e.code == 400 or e.code == 422:
            # Multiplex GET rejected entirely — likely patched or hardened
            print("PATCHED — server rejected multiplex GET (HTTP {})".format(e.code))
            sys.exit(0)
        print(f"UNKNOWN — HTTP {e.code}: {e.reason}")
        sys.exit(2)
    except Exception as e:
        print(f"UNKNOWN — connection error: {e}")
        sys.exit(2)

    if not isinstance(body, list) or len(body) < 2:
        print("UNKNOWN — unexpected response shape (not a multiplex array)")
        sys.exit(2)

    slot0 = body[0].get("data", {})
    slot1 = body[1].get("data", {})

    # Check for query-swap: slot 0 should have currentUser, slot 1 should have metadata.
    # If slot 0 contains metadata keys, the swap occurred.
    slot0_has_metadata = "metadata" in slot0 or "version" in slot0
    slot0_has_user = "currentUser" in slot0
    slot1_has_metadata = "metadata" in slot1

    if slot0_has_metadata and not slot0_has_user:
        print("VULNERABLE — multiplex query-swap confirmed (slot 0 returned slot 1 data)")
        print(f"  slot 0 data: {json.dumps(slot0)}")
        print(f"  slot 1 data: {json.dumps(slot1)}")
        sys.exit(1)
    elif slot0_has_user and slot1_has_metadata:
        print("PATCHED — slots are properly isolated")
        print(f"  slot 0 data: {json.dumps(slot0)}")
        print(f"  slot 1 data: {json.dumps(slot1)}")
        sys.exit(0)
    else:
        print("UNKNOWN — could not determine swap behavior")
        print(f"  slot 0 data: {json.dumps(slot0)}")
        print(f"  slot 1 data: {json.dumps(slot1)}")
        sys.exit(2)

if __name__ == "__main__":
    main()