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.
5 steps from start to impact.
Identify internet-facing GitLab instance
/api/graphql endpoint is enabled by default and does not require authentication for introspection or queries against public resources.- Network reachability to target GitLab instance on HTTP/HTTPS
- Internal-only GitLab deployments are unreachable without prior network compromise
X-Gitlab-Meta header; runZero asset inventory scansEnumerate public projects
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.- At least one project with
publicvisibility on the target instance
- Enterprises running fully private GitLab instances with no public projects are not exploitable via this path
visibility: public enumeration patternsCraft malicious @gl_introduced directive
@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.- Knowledge of the directive abuse technique (public PoCs available)
- None — multiple public PoCs exist, exploit is a single HTTP request
@gl_introduced in request body; nuclei template available (non-destructive, invokes touch method)FutureFieldFallback synthesizes unresolved field
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.- 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)
- Already-patched instances (18.11.11+, 19.0.8+, 19.1.6+, 19.2.4+) reject the crafted field
public_send callsDestructive method executes on target project
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.- Successful method dispatch from Step 4
- Impact limited to public projects only; private/internal projects are unreachable
ProjectDestroyWorker Sidekiq jobs/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; }/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.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.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).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).- 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.
The supporting signals.
| In-the-Wild Exploitation | Confirmed 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 Availability | Multiple 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 Score | 0.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 Status | Not listed as of 2026-10-08. Given confirmed exploitation, KEV addition is expected but has not occurred. Treat as KEV-equivalent for prioritization. |
| CVSS Vector | CVSS: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 Versions | GitLab 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 Versions | 18.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 Exposure | Shodan/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 & Attribution | Disclosed 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 Vulnerability | CVE-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.
- Horizon3.ai — CVE-2026-19478 Vulnerability Analysis
- CyCognito — Emerging Threat: GitLab Unauthenticated Project Deletion
- Help Net Security — Critical GitLab flaw allows attackers to modify or delete public projects
- The Hacker News — GitLab CVE-2026-19478 Comes Under Active Exploitation
- punitdarji/Gitlab-CVE-2026-19478 — Full PoC and Lab
- OX Security — Critical and High-Severity GraphQL CVEs in GitLab
- Greenbone — GitLab GraphQL Flaw Actively Exploited: Patch Now
- SOCPrime — CVE-2026-19478 Analysis
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
destroycall 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.
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.
#!/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