← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2021-22205 · CWE-94 · Disclosed 2021-04-23

An issue has been discovered in GitLab CE/EE affecting all versions starting from 11

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a loading dock that lets a fake shipping label drive a forklift into your server room

CVE-2021-22205 is GitLab CE/EE remote code execution caused by GitLab Workhorse handing uploaded jpg|jpeg|tiff files to a vulnerable ExifTool parser. GitLab and NVD track affected self-managed versions as >=11.9,<13.8.8, >=13.9,<13.9.6, and >=13.10,<13.10.3; the April 14, 2021 fixes landed in 13.8.8, 13.9.6, and 13.10.3. The root cause is the bundled ExifTool path behind image processing, so a crafted image can become code execution as the git service account.

The vendor's CRITICAL 10.0 is mostly justified in practice because this became an unauthenticated internet-reachable RCE, public exploits exist, and CISA KEV confirms real-world exploitation. The only meaningful downward pressure is population: this hits self-managed GitLab, not GitLab.com, and private/VPN-only instances are much safer. But for any internet-facing unpatched instance, this is still an edge compromise of a CI/source-code platform, which is exactly the kind of foothold attackers monetize.

"KEV-listed unauthenticated edge RCE on a source-control crown jewel stays critical, even with limited exposure population."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Fingerprint exposed GitLab

Attackers use internet scanning and passive fingerprinting to find self-managed GitLab instances and estimate version exposure. Censys published a hash-based method for public assets, and Rapid7 estimated roughly 60,000 internet-facing instances in late October 2021, with about 50% unpatched at that time.
Conditions required:
  • Target GitLab web UI is reachable from the internet over HTTP(S)
  • Instance is self-managed rather than GitLab.com
Where this breaks in practice:
  • Private-only deployments behind VPN, reverse proxy allowlists, or internal-only routing disappear from this step
  • Version fingerprinting is imperfect on some builds and can produce an uncertain bucket
Detection/coverage: External ASM/EASM and internet exposure scanners catch this well; internal vuln scanners only help if they can authenticate or fingerprint the GitLab build.
STEP 02

Reach an upload path into Workhorse

Public exploit chains use curl, Packet Storm PoCs, or custom scripts to POST a crafted image to an upload endpoint that GitLab Workhorse will process. The decisive detail is that GitLab reclassified this from authenticated to unauthenticated in September 2021, meaning defenders should assume some upload paths are reachable without a valid account in exposed deployments.
Conditions required:
  • An anonymous or trivially reachable upload path exists on the target instance
  • GitLab Workhorse is configured normally and image processing is enabled
Where this breaks in practice:
  • Closed registration, reduced public surface, and stricter workflow design can remove easy anonymous entry points on some deployments
  • WAFs may disrupt obvious multipart exploit traffic, though they are not a reliable control here
Detection/coverage: Look for suspicious multipart POSTs, odd random upload paths, and image uploads followed by parser activity; network IDS and reverse-proxy logs can sometimes see this, but generic web scanners often miss the exploitability nuance.
STEP 03

Exploit ExifTool via malicious image payload

The weaponized file abuses ExifTool's DjVu parsing flaw (CVE-2021-22204) while disguised as a normal image extension acceptable to GitLab's upload flow. When GitLab passes the file to ExifTool for metadata handling, attacker-controlled content is evaluated and arbitrary commands execute as the git user.
Conditions required:
  • GitLab version is in one of the vulnerable ranges
  • Bundled or linked ExifTool path remains vulnerable or un-hotpatched
Where this breaks in practice:
  • Fixed versions 13.8.8, 13.9.6, and 13.10.3 or the GitLab hotpatch stop this step
  • Disabling the ExifTool path or removing public image-upload exposure sharply reduces exploitability
Detection/coverage: Good Nuclei/Tsunami-style checks exist for exposure, but exploit detection is weaker. On-host review of GitLab logs, upload artifacts, and anomalous spawned processes is more reliable than perimeter signatures.
STEP 04

Turn git-user code exec into platform compromise

Initial execution lands as the git service account, which is already enough to threaten repositories, CI variables, tokens, webhooks, and supply-chain trust. Real operators then pivot to persistence, secret theft, or runner abuse; GitLab's own compromise guidance warns that post-exploit scripts and crontab persistence may survive after later patching.
Conditions required:
  • The git account can access sensitive repos, CI/CD material, or lateral movement paths
  • Post-exploitation monitoring is weak enough to miss persistence
Where this breaks in practice:
  • git is not automatically root, so full host takeover may require additional local privilege escalation or misconfiguration
  • Isolated runners, strong secret hygiene, and host EDR can limit blast radius
Detection/coverage: EDR, auditd/process telemetry, cron integrity checks, unexpected SSH key changes, suspicious API activity, and secret-access anomalies are the best places to catch this phase.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusConfirmed exploited. GitLab said on 2021-11-04 that it had confirmed exploitation on self-managed public-facing instances, and Rapid7 reported exploitation in the wild.
KEV statusCISA KEV listed on 2021-11-03 with due date 2021-11-17. That is strong evidence this is not a lab-only bug.
PoC / weaponizationPublic weaponization is easy: NVD/MITRE reference Packet Storm exploits, Rapid7 noted multiple public exploits, and GitLab later warned customers that a publicly available exploit existed.
EPSS0.94467 from the prompt intel, which implies extremely high exploitation likelihood. EPSS should be treated as supporting evidence only here because actual exploitation evidence supersedes it.
CVSS meaningCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H means unauthenticated network RCE with full triad impact. In plain English: if the box is reachable and vulnerable, the attacker does not need a user click or valid account.
Affected versionsAuthoritative affected ranges are >=11.9,<13.8.8, >=13.9,<13.9.6, and >=13.10,<13.10.3 for both CE and EE self-managed deployments.
Fixed versionsVendor fixes shipped in 13.8.8, 13.9.6, and 13.10.3 on 2021-04-14. GitLab also published a hotpatch for customers who could not upgrade immediately. *Inference:* distro-packaged GitLab may carry backports, so validate against distro advisories/package changelogs instead of trusting upstream semver alone.
Exposure dataRapid7 estimated ~60,000 internet-facing GitLab instances in late October 2021; Censys independently found 20,524 internet-facing hosts running vulnerable versions. This is not ubiquitous like Apache, but it is large enough to drive mass exploitation.
Disclosure / timelinePatched on 2021-04-14; CVE published 2021-04-23; GitLab revised the bug from authenticated to unauthenticated on 2021-09-21; KEV added it on 2021-11-03.
Researcher / reportingGitLab credited vakzz via HackerOne. GitLab, Rapid7, CERT-EU, and Censys all published follow-on analysis, which is why attacker knowledge and defender guidance became broadly available.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to CRITICAL (9.8/10)

The single biggest driver is that this is an unauthenticated, internet-facing RCE on a code-hosting and CI/CD platform with confirmed exploitation. The only real downward adjustment from a perfect 10 is that the vulnerable population is narrower than the CVSS implies: self-managed GitLab only, with materially less risk on private or VPN-only deployments.

HIGH Exploitability on internet-facing unpatched self-managed GitLab
HIGH Active exploitation / KEV significance
MEDIUM Population-level exposure in your specific estate

Why this verdict

  • Baseline stays brutal: vendor score 10.0 is anchored in an unauthenticated network RCE with no user interaction.
  • KEV and exploitation keep it at the top: CISA KEV plus GitLab/Rapid7 reporting means this is not theoretical and not waiting for an attacker to innovate.
  • Public-edge precondition is common enough: internet-facing GitLab is a realistic enterprise pattern, and exposed source-control platforms are high-value targets.
  • Blast radius is worse than host-only RCE: compromise of GitLab can expose repositories, CI/CD secrets, deploy keys, runners, and downstream software supply chain trust.
  • Slight downward pressure exists: this is not every GitLab deployment, GitLab.com is unaffected, and private/VPN-only instances remove the unauthenticated internet path.

Why not higher?

There is no practical bucket above CRITICAL, and that is exactly why this lands here. The only reason the score is not a clean 10.0 is environmental narrowing: self-managed only, reduced risk on non-internet-facing deployments, and initial execution is typically as git rather than immediate root.

Why not lower?

A lower severity would ignore the most important facts: no auth required, public exploit availability, and KEV-confirmed exploitation. This is not a post-initial-access bug or a narrow admin-only feature flaw; it is an edge RCE against a platform that often contains crown-jewel developer secrets.

05 · Compensating Control

What to do — in priority order.

  1. Pull public GitLab behind VPN or IP allowlists — Remove the unauthenticated internet path first. Because this CVE is KEV-listed, treat this as patch / mitigate immediately, within hours; if the UI does not need to be public, make it non-public before you finish normal patch change control.
  2. Apply the GitLab hotpatch if upgrade is blocked — GitLab published a hotpatch specifically for customers who could not move to fixed versions immediately. Use it only as an emergency bridge and deploy it within hours on exposed systems while you prepare the full vendor upgrade.
  3. Upgrade to 13.8.8 / 13.9.6 / 13.10.3 or later supported releases — The permanent fix is the vendor release, not a compensating story. For exposed or internet-reachable instances, do this immediately, within hours because active exploitation overrides the usual critical-window pacing.
  4. Hunt for post-exploit persistence — GitLab's own incident guidance warns that scripts and crontab persistence may remain even after patching. Start this review within hours on any system that was public-facing and vulnerable, because patching alone does not evict a prior compromise.
  5. Rotate GitLab-adjacent secrets after suspected exposure — If a vulnerable internet-facing instance was exposed, assume repo access, CI variables, tokens, deploy keys, and webhook secrets may have been read. Begin rotation within hours for any suspected-compromise system to reduce supply-chain follow-on risk.
What doesn't work
  • Relying on MFA does not help because the exploit path is unauthenticated and does not need account takeover.
  • A generic WAF-only posture is weak here; the payload is a crafted image processed by backend tooling, and exploit traffic may look like ordinary multipart upload traffic.
  • Patching after exploitation without host triage does not remove persistence; GitLab explicitly warned that malicious scripts or cron entries may survive an upgrade.
06 · Verification

Crowdsourced verification payload.

Run this on the target GitLab host as root or with sudo, because it reads local install paths and may call gitlab-rake. Save as verify-cve-2021-22205.sh and run sudo bash verify-cve-2021-22205.sh for auto-detect, or sudo bash verify-cve-2021-22205.sh 13.10.2 to test a specific version string. It outputs VULNERABLE, PATCHED, or UNKNOWN and exits 1, 0, or 2 respectively.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# verify-cve-2021-22205.sh
# Best-effort local checker for GitLab CE/EE CVE-2021-22205.
# Exit codes:
#   0 = PATCHED / not affected
#   1 = VULNERABLE
#   2 = UNKNOWN / unable to determine

set -u

normalize_version() {
  local v="$1"
  v="${v#v}"
  v="${v%%-*}"
  printf '%s' "$v"
}

is_semver_like() {
  [[ "$1" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]
}

ver_lt() {
  [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" != "$2" ] && [ "$1" != "$2" ]
}

ver_ge() {
  [ "$1" = "$2" ] || ! ver_lt "$1" "$2"
}

get_version() {
  local v=""

  if [ $# -ge 1 ] && [ -n "$1" ]; then
    printf '%s' "$1"
    return 0
  fi

  if command -v gitlab-rake >/dev/null 2>&1; then
    v=$(gitlab-rake gitlab:env:info 2>/dev/null | awk -F': ' '/GitLab version/ {print $2; exit}')
    if [ -n "$v" ]; then
      printf '%s' "$v"
      return 0
    fi
  fi

  if [ -r /opt/gitlab/embedded/service/gitlab-rails/VERSION ]; then
    v=$(head -n1 /opt/gitlab/embedded/service/gitlab-rails/VERSION 2>/dev/null || true)
    if [ -n "$v" ]; then
      printf '%s' "$v"
      return 0
    fi
  fi

  if [ -r /home/git/gitlab/VERSION ]; then
    v=$(head -n1 /home/git/gitlab/VERSION 2>/dev/null || true)
    if [ -n "$v" ]; then
      printf '%s' "$v"
      return 0
    fi
  fi

  return 1
}

main() {
  local raw_version version

  if ! raw_version=$(get_version "${1-}"); then
    echo "UNKNOWN - could not determine local GitLab version"
    exit 2
  fi

  version=$(normalize_version "$raw_version")

  if ! is_semver_like "$version"; then
    echo "UNKNOWN - detected version '$raw_version' is not a simple upstream semver; check distro backports/package changelog manually"
    exit 2
  fi

  # Affected ranges from GitLab/NVD:
  #   >=11.9,<13.8.8
  #   >=13.9,<13.9.6
  #   >=13.10,<13.10.3

  if ver_ge "$version" "13.10" && ver_lt "$version" "13.10.3"; then
    echo "VULNERABLE - GitLab version $version is in affected range >=13.10,<13.10.3"
    exit 1
  fi

  if ver_ge "$version" "13.9" && ver_lt "$version" "13.9.6"; then
    echo "VULNERABLE - GitLab version $version is in affected range >=13.9,<13.9.6"
    exit 1
  fi

  if ver_ge "$version" "11.9" && ver_lt "$version" "13.8.8"; then
    echo "VULNERABLE - GitLab version $version is in affected range >=11.9,<13.8.8"
    exit 1
  fi

  if ver_lt "$version" "11.9"; then
    echo "PATCHED - GitLab version $version is below the affected floor (not affected by CVE-2021-22205)"
    exit 0
  fi

  echo "PATCHED - GitLab version $version is outside the published affected ranges"
  exit 0
}

main "$@"
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, assume every internet-facing self-managed GitLab below 13.8.8/13.9.6/13.10.3 is an emergency: use the noisgate mitigation SLA override for KEV and patch / mitigate immediately, within hours by pulling it behind VPN/allowlists or applying GitLab's hotpatch if you cannot upgrade on the spot. Then complete the vendor upgrade and compromise review first on public instances, with full patch closure for any remaining exceptions inside the noisgate remediation SLA of ≤90 days for a CRITICAL finding—though for exposed systems, waiting anywhere near that long would be operationally indefensible.

Sources

  1. GitLab critical security release (fixed versions)
  2. GitLab action needed blog (confirmed exploitation, hotpatch)
  3. NVD CVE-2021-22205
  4. CISA KEV catalog print entry
  5. Rapid7 exploited-in-the-wild analysis
  6. Censys exposure analysis
  7. CVE record at CVE.org
  8. FIRST EPSS documentation
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.