← Back to Feed CACHED · 2026-10-07 20:09:46 · CACHE_KEY CVE-2026-19478
CVE-2026-19478 · CWE-94 · Disclosed 2026-08-17

GitLab CE/EE Unauthenticated GraphQL Directive Code Injection via @gl_introduced

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

Someone left a master key in the GraphQL schema and any stranger can use it to burn down your code vault

CVE-2026-19478 is an unauthenticated code injection flaw in GitLab CE/EE's GraphQL API layer. The @gl_introduced directive — designed to gate fields for rolling deployments — can be abused by referencing a future version number (e.g., 99.0). When GitLab's FutureFieldFallback logic synthesizes a field without an explicit resolver, graphql-ruby defaults to calling object.public_send(field_name) on the underlying Ruby ActiveRecord model, letting an attacker invoke zero-argument methods like destroy, archive, transfer, or attributes on any public project — no login required. Affected versions span 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. GitLab.com and Dedicated were pre-patched; only self-managed instances require action.

GitLab's vendor CRITICAL / 9.4 rating is accurate and, if anything, conservative. The CVSS vector (AV:N/AC:L/PR:N/UI:N) reflects the reality: zero authentication, zero user interaction, low complexity. The Confidentiality impact is scored Low because the attributes method leaks limited DB fields, but the Integrity and Availability impacts are both High — an attacker can permanently delete source code, corrupt CI/CD pipelines, and tamper with infrastructure-as-code. Active exploitation was confirmed within 48 hours of disclosure by watchTowr's honeypot network, multiple public PoCs exist, and EPSS has climbed to 0.60 (99th+ percentile). For a component that IS your CI/CD supply chain, the vendor score stands.

"Unauthenticated GraphQL directive injection lets anyone destroy public GitLab projects. Patch now."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Identify internet-facing GitLab instance

The attacker scans for self-managed GitLab instances using Shodan, Censys, or FOFA. Shodan/Censys report 20,000+ internet-reachable self-managed GitLab instances as of August 2026. The /api/graphql endpoint is enabled by default and does not require authentication for introspection or queries against public resources.
Conditions required:
  • Network reachability to target GitLab instance on HTTP/HTTPS
Where this breaks in practice:
  • Internal-only GitLab deployments are unreachable without prior network compromise
Detection/coverage: Shodan/Censys/FOFA queries for X-Gitlab-Meta header; runZero asset inventory scans
STEP 02

Enumerate public projects

The attacker queries the GraphQL API with a standard projects(visibility: public) query to enumerate available targets. No authentication is required. Public projects are the only attack surface; private and internal projects are not reachable via this chain.
Conditions required:
  • At least one project with public visibility on the target instance
Where this breaks in practice:
  • Enterprises running fully private GitLab instances with no public projects are not exploitable via this path
Detection/coverage: GraphQL query logging; WAF rules matching visibility: public enumeration patterns
STEP 03

Craft malicious @gl_introduced directive

The attacker constructs a GraphQL query referencing the @gl_introduced(version: "99.0") directive on a field name that maps to a destructive Ruby method — e.g., destroy, archive, or transfer. The PoC by punitdarji demonstrates this as a single HTTP POST to /api/graphql. watchTowr reproduced a working exploit 'within minutes' of reading the patch diff.
Conditions required:
  • Knowledge of the directive abuse technique (public PoCs available)
Where this breaks in practice:
  • None — multiple public PoCs exist, exploit is a single HTTP request
Detection/coverage: WAF/reverse-proxy rules matching @gl_introduced in request body; nuclei template available (non-destructive, invokes touch method)
STEP 04

FutureFieldFallback synthesizes unresolved field

GitLab's IntroducedTracer strips the directive-tagged field during parsing, then restores it during execution. FutureFieldFallback creates a new GraphQL field without a resolver class. Without a resolver, graphql-ruby calls object.public_send(field_name) on the Project ActiveRecord model, dispatching whatever method the attacker named as the field.
Conditions required:
  • Target runs an unpatched version (18.2–18.11.10, 19.0–19.0.7, 19.1–19.1.5, 19.2–19.2.3)
Where this breaks in practice:
  • Already-patched instances (18.11.11+, 19.0.8+, 19.1.6+, 19.2.4+) reject the crafted field
Detection/coverage: Application-level logging of unresolved field dispatch; Sentry/exception monitoring for unexpected public_send calls
STEP 05

Destructive method executes on target project

The dispatched method runs with the application's database privileges. destroy permanently deletes the project and all associated data (repos, CI/CD configs, wikis, issues). attributes dumps the project's ActiveRecord columns, leaking internal metadata. archive renders the project read-only. transfer can reassign project ownership. There is no undo for destroy beyond backup restoration.
Conditions required:
  • Successful method dispatch from Step 4
Where this breaks in practice:
  • Impact limited to public projects only; private/internal projects are unreachable
Detection/coverage: GitLab audit logs showing project deletion/archival without an authenticated actor; SIEM alerts on anomalous ProjectDestroyWorker Sidekiq jobs
03 · Compensating Control

1
CRITICAL 9.4→LOW 2.0
SEVERITY REDUCED
Block @gl_introduced in GraphQL requests at the reverse proxy / WAF — Add a WAF or nginx/Apache ModSecurity rule that rejects any POST to /api/graphql containing the string @gl_introduced. This breaks the exploit's entry point without affecting normal GitLab operation since @gl_introduced is an internal schema directive never sent by legitimate clients. Deploy within the noisgate mitigation SLA of ≤3 days for CRITICAL severity. Example nginx rule: if ($request_body ~* "@gl_introduced") { return 403; }
2
CRITICAL 9.4→MEDIUM 5.5
SEVERITY REDUCED
Restrict unauthenticated access to /api/graphql — Configure your reverse proxy to require authentication (e.g., basic auth, SSO, or IP allowlist) before requests reach /api/graphql. This eliminates the unauthenticated attack vector entirely. Note: this may break legitimate unauthenticated GraphQL consumers (e.g., public badge endpoints, some CI integrations). Deploy within 3 days per noisgate mitigation SLA.
3
CRITICAL 9.4→LOW 2.5
SEVERITY REDUCED
Set all projects to private or internal visibility — Change project visibility from public to internal or private for all projects. The exploit can only target public-visibility projects. This is a temporary measure — some projects may legitimately need public access. Deploy within 3 days.
4
CRITICAL 9.4→IGNORE 0.0
SEVERITY REDUCED
Upgrade to patched version (18.11.11, 19.0.8, 19.1.6, or 19.2.4) — Apply the vendor patch. The fix validates GraphQL field names against the schema and prevents arbitrary method dispatch via FutureFieldFallback. Zero-downtime upgrade supported, no database migrations required. This is the definitive remediation — deploy within the noisgate remediation SLA of ≤90 days, but given active exploitation, treat as immediate (within hours).
5
CRITICAL 9.4→HIGH 7.0
SEVERITY REDUCED
Verify backup integrity and test restoration — Confirm that GitLab backup jobs (gitlab-backup create) are running and that restoration has been tested. If an attacker has already invoked destroy on a project, the only recovery path is backup restoration. This does not prevent exploitation but limits blast radius to the recovery point objective (RPO).
What doesn't work
  • IP-based rate limiting alone — the exploit is a single HTTP request; rate limiting won't trigger before the destructive action completes.
  • GitLab's built-in abuse detection — the exploit bypasses GitLab's application-layer auth entirely; abuse detection only monitors authenticated sessions.
  • Network segmentation without proxy rules — putting GitLab on an internal network helps if it's truly unreachable from the internet, but doesn't protect against insider threats or compromised internal hosts that can reach the GraphQL endpoint.
  • Disabling GraphQL introspection — the attacker doesn't need introspection; the exploit payload is static and doesn't require schema discovery.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationConfirmed active. watchTowr detected exploitation against honeypot infrastructure within 48 hours of disclosure (Aug 17–19, 2026). Rescana and SmartTech247 corroborated ongoing campaigns targeting internet-facing instances. Not yet on CISA KEV.
Proof-of-Concept AvailabilityMultiple public PoCs. punitdarji/Gitlab-CVE-2026-19478 (Python, full lab + exploit script), renzi25031469/CVE-2026-19478, dinosn/gitlab-cve-2026-19478-lab (reproducible A/B lab). Non-destructive nuclei detection template also published.
EPSS Score0.60204 (99th+ percentile) — FIRST model assigns a 60.2% probability of exploitation in the next 30 days, placing this in the top fraction of all scored CVEs. Reflects confirmed active exploitation and low attack complexity.
KEV StatusNot listed as of 2026-10-08. Given confirmed exploitation, KEV addition is expected but has not occurred. Treat as KEV-equivalent for prioritization.
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H — Network-attackable, no privileges, no interaction. Confidentiality is Low (limited data leak via attributes), but Integrity and Availability are both High (project destruction/modification).
Affected VersionsGitLab CE/EE 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3. Versions prior to 18.2 are not affected. Note: 18.2–18.10.x have no direct patch — must upgrade to 18.11.11 branch.
Fixed Versions18.11.11, 19.0.8, 19.1.6, 19.2.4 (released 2026-08-17, emergency out-of-band). Zero-downtime upgrade supported, no new database migrations. GitLab.com and Dedicated pre-patched.
Internet ExposureShodan/Censys report 20,000+ internet-facing self-managed GitLab instances. CyCognito corroborated this figure. Not all will have public projects, but a significant fraction will.
Disclosure & AttributionDisclosed 2026-08-17 via emergency advisory. Reported by researcher hiimguardian through GitLab's HackerOne bug bounty program. Companion flaw CVE-2026-19650 (CVSS 7.1, GraphQL CSRF) was patched simultaneously.
Related VulnerabilityCVE-2026-19650 (CVSS 7.1) — GraphQL multiplex CSRF allowing mutation execution via GET requests with user interaction. Same patch release. Different attack vector but targets the same GraphQL surface.

Sources.

  1. Horizon3.ai — CVE-2026-19478 Vulnerability Analysis
  2. CyCognito — Emerging Threat: GitLab Unauthenticated Project Deletion
  3. Help Net Security — Critical GitLab flaw allows attackers to modify or delete public projects
  4. The Hacker News — GitLab CVE-2026-19478 Comes Under Active Exploitation
  5. punitdarji/Gitlab-CVE-2026-19478 — Full PoC and Lab
  6. OX Security — Critical and High-Severity GraphQL CVEs in GitLab
  7. Greenbone — GitLab GraphQL Flaw Actively Exploited: Patch Now
  8. SOCPrime — CVE-2026-19478 Analysis
05 · The Call

Final Verdict
= UNCHANGED to CRITICAL (9.4/10)

Why this verdict

  • Zero-friction exploitation chain: The entire attack is a single unauthenticated HTTP POST to /api/graphql. No credentials, no user interaction, no exploit chaining, no memory corruption — just a crafted GraphQL query. Attack complexity doesn't get lower than this.
  • Active exploitation confirmed: watchTowr documented in-the-wild attacks within 48 hours of disclosure. EPSS at 0.60 (99th+ percentile) reflects the realized threat. Multiple public PoCs are available, including full lab environments. Treat as KEV-equivalent.
  • Role multiplier: GitLab is *canonically* a CI/CD platform and source code repository — it is listed explicitly in the high-value-role catalog under 'CI/CD + artifact stores.' ≥90% of self-managed GitLab installations ARE the organization's primary CI/CD and SCM platform. A successful destroy call permanently deletes source code, CI/CD pipeline definitions, infrastructure-as-code, and container registry artifacts. This is supply-chain-scale blast radius by definition. The verdict floor is CRITICAL.
  • 20,000+ internet-exposed instances: Shodan/Censys data confirms a large attack surface. While not all instances have public projects, the default GitLab configuration allows public project creation and the GraphQL endpoint is unauthenticated by default.
  • No compensating control by default: The GraphQL endpoint is enabled and unauthenticated out of the box. No WAF, no rate limit, no directive validation stands between the attacker and the vulnerable code path in a default installation.

Why not higher?

The CVSS vector already sits at 9.4, just below the maximum. The 0.6-point gap from 10.0 is appropriate: confidentiality impact is genuinely Low (only attributes method leaks data, and it's limited to ActiveRecord column values), the scope is Unchanged (no hypervisor/container escape), and the attack surface is limited to public-visibility projects. A 10.0 would require high confidentiality impact or scope change, neither of which applies here.

Why not lower?

Every downward pressure point fails to materialize. The attack requires zero authentication and zero user interaction — there is no credential barrier to erode. The chain is a single HTTP request with public PoCs, so complexity friction is absent. The affected component is canonically a CI/CD supply-chain platform where ≥10% of installs (realistically >90%) occupy the high-value role, setting a CRITICAL floor. Active exploitation and 0.60 EPSS eliminate any argument that this is theoretical. The only narrowing factor — requiring public projects — does not change the blast radius when the condition is met.

06 · Verification

Crowdsourced verification payload.

Run this script on each self-managed GitLab host as root (or any user that can read /opt/gitlab/version-manifest.txt or query dpkg/rpm). Example: sudo bash check_cve_2026_19478.sh. It checks the installed GitLab package version against known-vulnerable ranges and outputs VULNERABLE, PATCHED, or UNKNOWN.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_19478.sh — Detect CVE-2026-19478 (GitLab GraphQL directive injection)
# Run on target GitLab host as root or a user with read access to version files.
# Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN

set -euo pipefail

get_version() {
  # Try version-manifest first (Omnibus)
  if [[ -f /opt/gitlab/version-manifest.txt ]]; then
    head -1 /opt/gitlab/version-manifest.txt | awk '{print $2}'
    return
  fi
  # Try dpkg (Debian/Ubuntu)
  if command -v dpkg &>/dev/null; then
    local v
    v=$(dpkg -l gitlab-ce gitlab-ee 2>/dev/null | awk '/^ii/{print $3}' | head -1 | sed 's/-.*//')
    [[ -n "$v" ]] && echo "$v" && return
  fi
  # Try rpm (RHEL/CentOS)
  if command -v rpm &>/dev/null; then
    local v
    v=$(rpm -q gitlab-ce gitlab-ee 2>/dev/null | grep -v 'not installed' | head -1 | sed 's/gitlab-[ce]e-//;s/-.*//')
    [[ -n "$v" ]] && echo "$v" && return
  fi
  # Try gitlab-rake
  if command -v gitlab-rake &>/dev/null; then
    gitlab-rake gitlab:env:info 2>/dev/null | grep 'GitLab:' | awk '{print $2}'
    return
  fi
  echo ""
}

version_gte() {
  # Returns 0 if $1 >= $2 using sort -V
  [[ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -1)" == "$2" ]]
}

VERSION=$(get_version)

if [[ -z "$VERSION" ]]; then
  echo "UNKNOWN — Could not determine GitLab version. Is this a GitLab host?"
  exit 2
fi

echo "Detected GitLab version: $VERSION"

# Parse major.minor
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)

# Not affected: versions before 18.2
if [[ "$MAJOR" -lt 18 ]] || { [[ "$MAJOR" -eq 18 ]] && [[ "$MINOR" -lt 2 ]]; }; then
  echo "PATCHED — Version $VERSION is not in the affected range (pre-18.2)."
  exit 0
fi

# Branch: 18.x
if [[ "$MAJOR" -eq 18 ]]; then
  if version_gte "$VERSION" "18.11.11"; then
    echo "PATCHED — Version $VERSION >= 18.11.11."
    exit 0
  else
    echo "VULNERABLE — Version $VERSION is in affected range 18.2–18.11.10. Upgrade to 18.11.11+."
    exit 1
  fi
fi

# Branch: 19.0.x
if [[ "$MAJOR" -eq 19 ]] && [[ "$MINOR" -eq 0 ]]; then
  if version_gte "$VERSION" "19.0.8"; then
    echo "PATCHED — Version $VERSION >= 19.0.8."
    exit 0
  else
    echo "VULNERABLE — Version $VERSION is in affected range 19.0.0–19.0.7. Upgrade to 19.0.8+."
    exit 1
  fi
fi

# Branch: 19.1.x
if [[ "$MAJOR" -eq 19 ]] && [[ "$MINOR" -eq 1 ]]; then
  if version_gte "$VERSION" "19.1.6"; then
    echo "PATCHED — Version $VERSION >= 19.1.6."
    exit 0
  else
    echo "VULNERABLE — Version $VERSION is in affected range 19.1.0–19.1.5. Upgrade to 19.1.6+."
    exit 1
  fi
fi

# Branch: 19.2.x
if [[ "$MAJOR" -eq 19 ]] && [[ "$MINOR" -eq 2 ]]; then
  if version_gte "$VERSION" "19.2.4"; then
    echo "PATCHED — Version $VERSION >= 19.2.4."
    exit 0
  else
    echo "VULNERABLE — Version $VERSION is in affected range 19.2.0–19.2.3. Upgrade to 19.2.4+."
    exit 1
  fi
fi

# 19.3+ should include the fix
if [[ "$MAJOR" -eq 19 ]] && [[ "$MINOR" -ge 3 ]]; then
  echo "PATCHED — Version $VERSION is beyond the affected branches."
  exit 0
fi

if [[ "$MAJOR" -ge 20 ]]; then
  echo "PATCHED — Version $VERSION is beyond the affected branches."
  exit 0
fi

echo "UNKNOWN — Could not classify version $VERSION. Manual review required."
exit 2