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
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. |
noisgate verdict.
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.
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.
What to do — in priority order.
- 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.
- 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.
- 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.
- Hunt for post-exploit persistence — GitLab's own incident guidance warns that scripts and
crontabpersistence 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. - 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.
- 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.
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 "$@"
If you remember one thing.
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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.