← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2025-21613 · CWE-88 · Disclosed 2025-01-06

go-git is a highly extensible git implementation library written in pure Go

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

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.

"Vendor called it internet-RCE; reality is a niche library bug gated by file:// usage and attacker-controlled URLs."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find an app that embeds go-git

The attacker needs a reachable application or service that uses 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.
Conditions required:
  • Target application links a vulnerable go-git version
  • Attacker can submit or influence repository URLs
Where this breaks in practice:
  • 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
Detection/coverage: SCA, container scanners, and SBOM tooling generally catch the vulnerable module version well; network scanners do not, because this is not a network appliance signature.
STEP 02

Reach the file-transport code path

The exploit only matters if the application allows a 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.
Conditions required:
  • Application allows file:// URLs or local path semantics
  • Target host has a usable git binary in the execution environment
Where this breaks in practice:
  • Many production systems explicitly deny local file transport for repository imports
  • Some minimal containers do not ship the git CLI at all
Detection/coverage: Application-layer validation logs may reveal blocked file:// submissions; generic vuln scanners cannot prove reachability of this exact code path.
STEP 03

Inject git-upload-pack flags

Once the vulnerable path is hit, crafted URL input can set arbitrary values for 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.
Conditions required:
  • Attacker-controlled URL reaches vulnerable parsing and invocation logic
  • Underlying workflow actually performs a clone/fetch-style operation
Where this breaks in practice:
  • Impact is bounded by what git-upload-pack flags can influence
  • The vulnerable process still runs with the application's existing OS permissions
Detection/coverage: EDR or process telemetry can catch suspicious child git executions and unusual git-upload-pack argument patterns on build runners and app hosts.
STEP 04

Turn flag control into useful impact

Practical impact depends on what local repositories or files are reachable from the host and what the surrounding application does with the results. In the best attacker case this can expose code, secrets embedded in repositories, or disrupt repository operations; in weaker cases it just causes failed jobs or noisy build behavior. The blast radius is usually the single application host or runner, not the whole enterprise, unless that host already holds privileged source material.
Conditions required:
  • Host contains interesting local repositories or sensitive data paths
  • Compromised workflow has enough privilege to read or affect them
Where this breaks in practice:
  • 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
Detection/coverage: Host telemetry, CI job logs, and artifact-integrity monitoring are better detectors than perimeter controls.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo public evidence of active exploitation found in the sources reviewed, and not listed in CISA KEV.
Proof-of-concept availabilityNo 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.
EPSSUser-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 statusNot in KEV as of the reviewed CISA catalog page.
CVSS vector reality checkCVSS: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 versionsgithub.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 versionsUpstream 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 populationThere 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 date2025-01-06 in NVD/CVE publication data; GitHub advisory published January 5, 2025 and reviewed/published January 6, 2025.
Reporter / creditGitHub advisory credits @vin01 for responsible disclosure.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.6/10)

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.

HIGH Affected version range and fixed upstream version
HIGH Requirement for `file` transport to reach the vulnerable shell-out path
MEDIUM Real-world exploitability across enterprise app estates using go-git

Why this verdict

  • Major friction: requires file transport. The advisory explicitly limits exploitation to the file protocol 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 the git CLI 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.

05 · Compensating Control

What to do — in priority order.

  1. Block file:// and local-path repository URLs — Enforce URL-scheme allowlisting in every application path that accepts repository locations, permitting only approved schemes like https:// and ssh://. 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.
  2. Remove the system git binary from unneeded runtimes — This CVE only bites on the shell-out path, so minimal containers and app runtimes that do not require CLI git should 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.
  3. 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.
  4. Hunt for vulnerable modules in SBOMs and lockfiles — Query go.mod, go.sum, vendored modules, container SBOMs, and build images for github.com/go-git/go-git/v5 versions below 5.13.0 and gopkg.in/src-d/go-git.v4 through 4.13.1. Because this is a library issue, asset inventory matters more than perimeter scanning.
  5. Watch for anomalous child git executions — Add EDR or audit rules for service processes and CI runners spawning git-upload-pack with unexpected arguments. This does not prevent exploitation, but it gives you a practical tripwire on the narrow path where the bug becomes real.
What doesn't work
  • 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-git is 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 file transport.
06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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())
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like an all-hands internet emergency; treat it like a targeted dependency cleanup with architecture review. For a MEDIUM noisgate verdict there is noisgate mitigation SLA — go straight to the 365-day remediation window, so first identify every app, runner, and image that embeds vulnerable go-git and determine whether any of them accept attacker-influenced repository URLs or permit 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

  1. GitHub Advisory GHSA-v725-9546-7q7m
  2. NVD CVE-2025-21613
  3. CVE Record
  4. go-git releases
  5. Ubuntu CVE tracker
  6. Debian security tracker
  7. CISA KEV catalog
  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.