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.
4 steps from start to impact.
Fingerprint exposed GitLab
- Target GitLab web UI is reachable from the internet over HTTP(S)
- Instance is self-managed rather than GitLab.com
- 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
Reach an upload path into Workhorse
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.- An anonymous or trivially reachable upload path exists on the target instance
- GitLab Workhorse is configured normally and image processing is enabled
- 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
Exploit ExifTool via malicious image payload
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.- GitLab version is in one of the vulnerable ranges
- Bundled or linked ExifTool path remains vulnerable or un-hotpatched
- Fixed versions
13.8.8,13.9.6, and13.10.3or the GitLab hotpatch stop this step - Disabling the ExifTool path or removing public image-upload exposure sharply reduces exploitability
Turn git-user code exec into platform compromise
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.- The
gitaccount can access sensitive repos, CI/CD material, or lateral movement paths - Post-exploitation monitoring is weak enough to miss persistence
gitis not automaticallyroot, so full host takeover may require additional local privilege escalation or misconfiguration- Isolated runners, strong secret hygiene, and host EDR can limit blast radius
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.- 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.
The supporting signals.
| In-the-wild status | Confirmed 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 status | CISA KEV listed on 2021-11-03 with due date 2021-11-17. That is strong evidence this is not a lab-only bug. |
| PoC / weaponization | Public 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. |
| EPSS | 0.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 meaning | CVSS: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 versions | Authoritative 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 versions | Vendor 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 data | Rapid7 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 / timeline | Patched 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 / reporting | GitLab 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. |
Sources.
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.
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.
#!/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 "$@"