This is less an unlocked front door than a valet key that only works if your app willingly hands it to the attacker
CVE-2025-21613 is an argument-injection flaw in go-git before v5.13.0, with the legacy gopkg.in/src-d/go-git.v4 line also affected through 4.13.1. The bug lets attacker-controlled input in the repository URL influence git-upload-pack flags, but only when the application uses the file transport path, because that is the only go-git path that shells out to the system git binary. In plain English: this is not a generic network bug in every app that imports go-git; it is a bug in the subset of apps that accept untrusted repository URLs and then let those URLs resolve to local-file transport on the target host.
The vendor CVSS treats this like broad unauthenticated network compromise, which overstates enterprise risk for most deployments. The decisive friction is environmental: go-git is a library, not an exposed daemon, and exploitation requires both attacker influence over the URL field and an application path that reaches file:// or equivalent local transport behavior. That combination exists in some CI, GitOps, import, and automation workflows, but it is nowhere near internet-wide, and modern allowlisting of URL schemes often kills the path outright.
4 steps from start to impact.
Find an app that embeds go-git
github.com/go-git/go-git/v5 or the old v4 line for clone, fetch, pull, or repository import operations. Typical candidates are CI helpers, internal developer portals, Git import features, or automation jobs rather than a directly exposed go-git service, because go-git itself is just a library. Weaponization starts with identifying a code path that accepts a repository URL from an external or low-trust source.- Target application links a vulnerable
go-gitversion - Attacker can submit or influence repository URLs
- Many deployments use go-git only against fixed, internal, or admin-controlled repository URLs
- SBOM and SCA tools usually identify the dependency before attackers ever see the app behavior
Reach the file-transport code path
file transport URL or equivalent local-path handling that reaches the shell-out path to the local git binary. If the app restricts URLs to https:// or ssh://, this vulnerability is dead on arrival. The attacker therefore has to bypass or inherit permissive URL validation from the embedding application.- Application allows
file://URLs or local path semantics - Target host has a usable
gitbinary in the execution environment
- Many production systems explicitly deny local file transport for repository imports
- Some minimal containers do not ship the
gitCLI at all
file:// submissions; generic vuln scanners cannot prove reachability of this exact code path.Inject git-upload-pack flags
git-upload-pack flags. That gives the attacker leverage over how the host invokes the underlying git operation, with consequences dependent on the embedding app, local repository access, and execution context. This is more constrained than full shell metacharacter command injection; the primitive is control of flags to a specific git helper, not unconstrained OS command execution.- Attacker-controlled URL reaches vulnerable parsing and invocation logic
- Underlying workflow actually performs a clone/fetch-style operation
- Impact is bounded by what
git-upload-packflags can influence - The vulnerable process still runs with the application's existing OS permissions
git executions and unusual git-upload-pack argument patterns on build runners and app hosts.Turn flag control into useful impact
- Host contains interesting local repositories or sensitive data paths
- Compromised workflow has enough privilege to read or affect them
- Most hosts do not keep crown-jewel repositories in locations reachable to untrusted file-transport operations
- Least-privilege runners and ephemeral CI workers sharply reduce persistence and blast radius
The supporting signals.
| In-the-wild status | No public evidence of active exploitation found in the sources reviewed, and not listed in CISA KEV. |
|---|---|
| Proof-of-concept availability | No authoritative maintainer PoC was published in the advisory. Public third-party writeups discuss exploitability, but the primary sources reviewed do not include an official exploit repo. |
| EPSS | User-supplied EPSS is 0.03834 (~3.834% probability over 30 days), which is modest rather than urgent. A non-primary aggregator snapshot also places it around the mid-80th percentile. |
| KEV status | Not in KEV as of the reviewed CISA catalog page. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H assumes broad network reachability, but the advisory itself says exploitation only happens on the file transport path that shells out to local git. |
| Affected versions | github.com/go-git/go-git/v5 < 5.13.0; legacy gopkg.in/src-d/go-git.v4 >= 4.0.0, <= 4.13.1 per GitHub advisory. |
| Fixed versions | Upstream fix is 5.13.0 for v5. Distro backports exist, including Ubuntu 24.04: 5.4.2-4ubuntu0.24.04.3+esm2 and 22.04: 5.4.2-3ubuntu0.1~esm1; Debian notes unstable fixed at 5.13.2-1 with newer branches beyond that. |
| Exposure population | There is no direct Shodan/Censys-style exposure model for this CVE because go-git is a library, not a fingerprintable internet service. Exposure is inherited from applications that embed it and accept attacker-influenced repository URLs. |
| Disclosure date | 2025-01-06 in NVD/CVE publication data; GitHub advisory published January 5, 2025 and reviewed/published January 6, 2025. |
| Reporter / credit | GitHub advisory credits @vin01 for responsible disclosure. |
noisgate verdict.
The single biggest downgrade factor is that exploitation requires a very specific application behavior: accepting attacker-controlled repository URLs that reach the file transport shell-out path. That turns a vendor-labeled internet critical into a conditional, workflow-dependent library issue whose reachable population is far smaller than the CVSS headline suggests.
Why this verdict
- Major friction: requires
filetransport. The advisory explicitly limits exploitation to thefileprotocol path, which dramatically cuts the exposed population versus a true network-reachable parser or service bug. - It is a library, not a daemon. Attackers cannot scan the internet for
go-git; they need a separate vulnerable application that embeds it and accepts attacker-controlled repository URLs. - Attacker position is narrower than CVSS implies. In practice this often means authenticated or workflow-level influence over a repo-import field, CI job parameter, or internal automation input, which is post-initial-access or at least post-reachability in many enterprises.
- Modern controls often stop it early. URL-scheme allowlisting, denying
file://, ephemeral runners, and omission of thegitCLI in containers all break the chain before impact. - Threat intel is quiet. No KEV listing, no active exploitation evidence in reviewed primary sources, and the supplied EPSS is not screaming emergency.
Why not higher?
The attack chain has two compounding gates: the vulnerable dependency must exist, and the embedding application must permit attacker-influenced file transport. That is a much narrower and less internet-scalable path than the vendor's unauthenticated network-critical framing. There is also no reviewed evidence of active exploitation to justify keeping this in a top emergency bucket.
Why not lower?
This is still a real security flaw in a popular Git automation library, not a theoretical lint issue. If your estate includes self-service repo imports, GitOps controllers, CI utilities, or developer platforms that pass low-trust URLs into go-git, the exploit preconditions can be satisfied surprisingly easily. In those environments, compromise of a runner or access to sensitive local repositories can become meaningful business impact.
What to do — in priority order.
- Block
file://and local-path repository URLs — Enforce URL-scheme allowlisting in every application path that accepts repository locations, permitting only approved schemes likehttps://andssh://. For a MEDIUM verdict there is noisgate no mitigation SLA — go straight to the 365-day remediation window, but if the app is internet-facing or multi-tenant, do this control change immediately because it kills the exact vulnerable path. - Remove the system
gitbinary from unneeded runtimes — This CVE only bites on the shell-out path, so minimal containers and app runtimes that do not require CLIgitshould not ship it. That is a high-leverage containment step for runners and service containers and can usually be folded into the next base-image refresh even though there is no formal mitigation SLA for this severity. - Constrain repository operations to an allowlisted workspace — Run git workflows under dedicated service accounts with restricted filesystem access and ephemeral work directories. That limits what a successful exploit can read or disrupt and should be part of your normal hardening before the 365-day remediation window closes.
- Hunt for vulnerable modules in SBOMs and lockfiles — Query
go.mod,go.sum, vendored modules, container SBOMs, and build images forgithub.com/go-git/go-git/v5versions below5.13.0andgopkg.in/src-d/go-git.v4through4.13.1. Because this is a library issue, asset inventory matters more than perimeter scanning. - Watch for anomalous child
gitexecutions — Add EDR or audit rules for service processes and CI runners spawninggit-upload-packwith unexpected arguments. This does not prevent exploitation, but it gives you a practical tripwire on the narrow path where the bug becomes real.
- A WAF will not reliably help because the vulnerable sink is inside application logic and local git invocation, not a stable HTTP exploit signature.
- Perimeter network vulnerability scanning will miss most instances because
go-gitis an embedded dependency, not a bannered network service. - Generic MFA is irrelevant unless the only attacker path to the URL field is through an authenticated workflow; it does nothing to the vulnerable code path itself.
- Blocking only outbound internet git traffic is insufficient because the risky path here is specifically local
filetransport.
Crowdsourced verification payload.
Run this on a developer workstation, CI workspace, source repository root, or extracted SBOM/asset directory that contains go.mod, go.sum, or vendor/modules.txt. Invoke it as python3 check_cve_2025_21613.py /path/to/project; no admin privileges are required. It reports VULNERABLE if it finds github.com/go-git/go-git/v5 below 5.13.0 or gopkg.in/src-d/go-git.v4 at or below 4.13.1, PATCHED if it finds only fixed versions, and UNKNOWN if it cannot determine usage.
#!/usr/bin/env python3
# check_cve_2025_21613.py
# Defensive dependency check for CVE-2025-21613 in go-git
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=usage/error
import os
import re
import sys
from typing import List, Optional, Tuple
TARGETS = {
'github.com/go-git/go-git/v5': ('5.13.0', 'lt'),
'gopkg.in/src-d/go-git.v4': ('4.13.1', 'le'),
}
SEMVER_RE = re.compile(r'v?(\d+)\.(\d+)\.(\d+)')
REQ_RE = re.compile(r'^\s*require\s+([^\s]+)\s+(v?[^\s]+)')
REQ_BLOCK_RE = re.compile(r'^\s*([^\s]+)\s+(v?[^\s]+)')
MODLINE_RE = re.compile(r'^#\s+([^\s]+)\s+(v?[^\s]+)')
def parse_semver(v: str) -> Optional[Tuple[int, int, int]]:
m = SEMVER_RE.search(v)
if not m:
return None
return tuple(int(x) for x in m.groups())
def cmp_ver(a: str, b: str) -> Optional[int]:
pa = parse_semver(a)
pb = parse_semver(b)
if pa is None or pb is None:
return None
return (pa > pb) - (pa < pb)
def is_vulnerable(module: str, version: str) -> Optional[bool]:
fixed, op = TARGETS[module]
c = cmp_ver(version, fixed)
if c is None:
return None
if op == 'lt':
return c < 0
if op == 'le':
return c <= 0
return None
def collect_from_go_mod(path: str) -> List[Tuple[str, str, str]]:
results = []
if not os.path.isfile(path):
return results
in_block = False
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
s = line.strip()
if s.startswith('require ('):
in_block = True
continue
if in_block and s == ')':
in_block = False
continue
m = REQ_RE.match(line)
if m:
mod, ver = m.groups()
if mod in TARGETS:
results.append((mod, ver, path))
continue
if in_block:
m = REQ_BLOCK_RE.match(line)
if m:
mod, ver = m.groups()
if mod in TARGETS:
results.append((mod, ver, path))
return results
def collect_from_vendor_modules(path: str) -> List[Tuple[str, str, str]]:
results = []
if not os.path.isfile(path):
return results
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
m = MODLINE_RE.match(line)
if not m:
continue
mod, ver = m.groups()
if mod in TARGETS:
results.append((mod, ver, path))
return results
def collect_from_go_sum(path: str) -> List[Tuple[str, str, str]]:
results = []
if not os.path.isfile(path):
return results
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
parts = line.strip().split()
if len(parts) < 2:
continue
mod, ver = parts[0], parts[1]
if mod in TARGETS:
ver = ver.split('/go.mod')[0]
results.append((mod, ver, path))
return results
def main() -> int:
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 check_cve_2025_21613.py /path/to/project')
return 3
root = sys.argv[1]
if not os.path.exists(root):
print(f'UNKNOWN - path not found: {root}')
return 3
findings: List[Tuple[str, str, str]] = []
for base, dirs, files in os.walk(root):
if '.git' in dirs:
dirs.remove('.git')
if 'node_modules' in dirs:
dirs.remove('node_modules')
if 'vendor' in dirs and 'modules.txt' not in files:
pass
if 'go.mod' in files:
findings.extend(collect_from_go_mod(os.path.join(base, 'go.mod')))
if 'go.sum' in files:
findings.extend(collect_from_go_sum(os.path.join(base, 'go.sum')))
if 'modules.txt' in files and os.path.basename(base) == 'vendor':
findings.extend(collect_from_vendor_modules(os.path.join(base, 'modules.txt')))
if not findings:
print('UNKNOWN - no matching go-git modules found in go.mod, go.sum, or vendor/modules.txt')
return 2
vulnerable = []
patched = []
undetermined = []
seen = set()
for mod, ver, src in findings:
key = (mod, ver, src)
if key in seen:
continue
seen.add(key)
verdict = is_vulnerable(mod, ver)
entry = f'{mod} {ver} ({src})'
if verdict is True:
vulnerable.append(entry)
elif verdict is False:
patched.append(entry)
else:
undetermined.append(entry)
if vulnerable:
print('VULNERABLE - ' + '; '.join(vulnerable))
return 1
if patched and not undetermined:
print('PATCHED - ' + '; '.join(patched))
return 0
detail = []
if patched:
detail.append('patched=' + ', '.join(patched))
if undetermined:
detail.append('undetermined=' + ', '.join(undetermined))
print('UNKNOWN - ' + '; '.join(detail))
return 2
if __name__ == '__main__':
sys.exit(main())
If you remember one thing.
file:// transport. Where that path exists, block file:// immediately as a hardening measure even though there is no formal mitigation deadline, then move the actual upgrade to go-git v5.13.0+ or distro backports into the noisgate remediation SLA of ≤ 365 days; if you run self-service CI, GitOps, or repo-import features, compress that timeline materially and fix those first.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.