post-checkout hook that Git runs before git clone --recursive has even finished. No credentials, no second stage, no user interaction beyond a clone. Git rated it CVSS 8.1; CISA added it to the Known Exploited Vulnerabilities catalog with a federal remediation deadline, and public reporting confirms it is being used in the wild.
This is a bug in Git itself, not in any one repository host or CI product, and it lands on the Linux and macOS clients that most engineering and build infrastructure runs on. The fixes shipped across every supported release line on the same coordinated date, 8 July 2025: v2.43.7, v2.44.4, v2.45.4, v2.46.4, v2.47.3, v2.48.2, v2.49.1, and v2.50.1. If you run anything older than the fix for your line, and you ever clone repositories you do not fully control, you are exposed. Below is the mechanism from the byte up, why the ordinary --recursive flag is the whole attack surface, and what actually closes it. We do not publish a working exploit. We explain the shape precisely enough that you can find it on your own fleet. Verifiable security.
Root cause: a read that strips, a write that keeps
Git stores configuration, including submodule definitions, in a small text format used by .git/config, the global ~/.gitconfig, and the .gitmodules file that travels inside a repository. Every value in that format goes through two separate code paths over its life: a parser that reads a value out of the file, and a serializer that writes a value back into it. The vulnerability is an asymmetry between those two paths in how they treat one specific byte, the carriage return, \r, hexadecimal 0x0D.
On the read side, Git's config parser is tolerant of Windows-style line endings. When it reaches the end of a value and finds a trailing carriage return before the newline, it strips that carriage return so that a value written on a CRLF system reads back cleanly on a LF system. That behavior is reasonable in isolation. On the write side, Git only wraps a value in quotes when it contains a character that would otherwise be ambiguous, and a trailing carriage return is not on that list. So a value that ends in \r is serialized literally, unquoted, with the raw carriage return sitting just before the line break.
Put the two halves together and you get a value that does not survive a round trip. Write path\r and it lands in the file as path\r; read that same line back and the parser strips the carriage return, handing you path. The stored representation and the interpreted representation disagree by exactly one invisible byte. In a text field that would be a cosmetic annoyance. In a field that Git uses to decide where on disk to write files, it is a security boundary that silently moves.
The asymmetry at the heart of CVE-2025-48384: the serializer keeps a trailing carriage return, the parser strips it. Illustrative, not from any specific repository.
This is why the classification is a carriage-return interpretation conflict layered on top of CWE-59, improper link resolution before file access. Two components look at the same bytes and reach different conclusions about what they mean, and the gap between those conclusions is where the file gets written to the wrong place. The Windows client is not affected the same way, which is why this is scoped to the Linux and macOS builds that dominate developer laptops and CI runners.
Weaponization: a submodule that lands where it should not
Submodules are the natural place to turn this desync into a file write, because a submodule is, by definition, a set of files that Git checks out to a path taken from configuration. The .gitmodules file at the root of a repository declares each submodule with a name, a URL to fetch it from, and a path that says where in the working tree its contents belong. During a recursive clone, Git reads that path, records the submodule, fetches its objects, and writes its files to the named location. Every one of those steps trusts the path field.
An attacker who controls a repository controls its .gitmodules file, and therefore controls that path field down to the byte. Give a submodule a path with a trailing carriage return and the asymmetry does the rest. One part of Git validates and records the path with the carriage return present; another part, reading it back through the stripping parser, acts on the path without it. The submodule's files are written to a location that differs from the one Git believes it checked, and that difference is attacker-chosen. In effect, the malicious repository has an arbitrary file write into the directory tree that the clone touches, gated only by what the attacker can express as a path plus one trailing byte.
By itself, that is not yet code execution. It is a primitive: attacker-controlled contents, at an attacker-influenced path, produced automatically by a clone. The exploit is in aiming that primitive somewhere that Git will not just store but execute.
From arbitrary file write to code execution
Git has a built-in mechanism for running code at defined moments in its lifecycle: hooks. A hook is simply an executable file in the repository's .git/hooks/ directory with a well-known name. The post-checkout hook, for example, is run automatically by Git immediately after a checkout completes, and a recursive clone performs a checkout. If an executable file named post-checkout exists in .git/hooks/ at that moment, Git runs it, with the privileges of the user doing the clone. Hooks are deliberately not copied from a remote for exactly this reason: a repository is not supposed to be able to plant one.
The file-write primitive removes that guarantee. The malicious repository also ships a symlink, a perfectly ordinary object for Git to check out, pointing from an innocuous-looking name in the working tree into .git/hooks/. The submodule path, with its trailing carriage return, is crafted so that when the stripping parser resolves it, the submodule's contents are written through that symlink and land inside .git/hooks/ as an executable named for a hook such as post-checkout. The submodule's payload is now a hook. The very same git clone --recursive that fetched it then completes its checkout, sees the hook, and executes it. There is no second command, no separate build step, no waiting for a developer to run make. Cloning the repository is the trigger.
Cloning the repository is the trigger. The attacker does not wait for you to build, test, or run the code. Git executes their hook before the clone command returns.
That collapse of the whole chain into one command is what makes CVE-2025-48384 severe rather than merely clever. The asymmetry provides the write, the symlink provides the aim, and Git's own hook execution provides the run, all inside one invocation that engineers and automation issue thousands of times a day.
Blast radius: everywhere --recursive meets untrusted code
The trigger is git clone --recursive (equivalently --recurse-submodules, or an ordinary clone followed by git submodule update --init) against a repository the attacker can influence. That is not an exotic condition. It is one of the most common operations in modern software delivery, and it runs in several places that matter:
- CI and build runners. Pipelines routinely clone repositories recursively to pull in submodule dependencies, often on ephemeral runners with broad network access and cached credentials. A poisoned dependency or a pull request from a fork that alters
.gitmodulescan run code on the runner during checkout, before any test or build script the pipeline author wrote gets a say. - Developer laptops. Cloning an unfamiliar repository to read it, review it, or evaluate a library is a daily habit, and
--recursiveis muscle memory for anything with submodules. The bug executes on the developer's own machine, with the developer's own environment and tokens. - Dependency vendoring and mirroring. Systems that vendor source, mirror upstreams, or build software bills of materials frequently clone third-party repositories recursively on a schedule, unattended, with automation privileges.
- IDE and tooling auto-clone. Editors, package managers, and scaffolding tools that clone repositories on the user's behalf can pull submodules automatically, so a user who never types a Git command can still trigger the checkout.
Separate this from the class of GitHub Actions issues that leak secrets through workflow logs or the GITHUB_TOKEN. This is not a log-exposure or token-scope problem, and it does not depend on any one hosting platform's CI features. It is code execution in the Git client at clone time, so it applies equally to a self-hosted runner, a corporate build farm, an air-gapped mirror, and a laptop cloning from any remote. The common ingredient is not a platform. It is an out-of-date Git binary meeting a repository someone else can write to.
Detection: version, contents, and clone-time writes
You can assess and hunt this without ever running the exploit, because the exposure is determined by facts you can read directly.
- Inventory Git client versions across every place a clone happens. This is the highest-signal fact. Enumerate the
git --versionof developer workstations, CI runner images, base container images, build servers, and any service that shells out to Git. Compare each against the fixed release for its line. A pre-fix version that ever clones untrusted repositories is exposed, full stop. - Scan repositories for hostile
.gitmodulesentries. Inspect.gitmodulesfiles for submodule paths that contain raw carriage-return bytes (0x0D), that resolve into.git/or a hooks directory, or that pair with a symlink pointing into.git/. A path field with an embedded control byte has no legitimate use and is a strong indicator on its own. - Alert on hook files created during a clone. Any file appearing in
.git/hooks/as part of a clone or submodule checkout is anomalous, because hooks are never fetched from a remote by design. Watching for the creation of executable hook files during clone operations catches the payload at the exact moment it lands, independent of the specific path trick used to place it.
Be precise about scope, because precision is the point. Not every clone is dangerous: cloning your own repositories, or repositories from a fully trusted internal remote, does not expose you to this even on an unpatched client. The risk is specifically the intersection of a pre-fix Git version and a clone of code that someone outside your trust boundary can write to. The work is to find that intersection on your fleet rather than to assume it is everywhere or nowhere.
Remediation: patch the client, contain --recursive
Close CVE-2025-48384 across your fleet
- Upgrade every Git client to the fixed build for its line. That is
2.43.7,2.44.4,2.45.4,2.46.4,2.47.3,2.48.2,2.49.1,2.50.1, or newer. Do not forget the Git baked into container base images, CI runner images, and developer tooling, which lags desktop package managers. - Enforce a minimum Git version in CI. Add a check that fails the pipeline if the runner's
git --versionis below the fix. This turns an easily-forgotten upgrade into a gate that cannot silently regress when a base image is rebuilt. - Do not clone untrusted repositories with
--recursive. Where you must handle code from outside your trust boundary, clone without submodules first, inspect the.gitmodulesfile for anomalous paths, then initialize submodules deliberately rather than automatically. - Treat fork pull requests as untrusted input to checkout. A pull request that modifies
.gitmodulesis modifying code that your runner will execute at clone time. Gate recursive checkout of fork branches behind the same review you apply to any untrusted code. - Constrain runner privilege and network. A least-privilege runner with scoped, short-lived credentials shrinks the value of clone-time execution even before every client is patched.
- Watch
.git/hooks/for clone-time writes. A hook file that appears during a clone is never legitimate; make its creation a high-signal alert on build infrastructure.
The durable fix is the client upgrade, because it removes the asymmetry itself: the patched serializer no longer emits a bare trailing carriage return, so the round trip is faithful and the submodule can no longer be aimed away from its declared path. Everything else on the list is defense in depth for the window before every last Git binary in your estate is current, which, on real fleets with base images and vendored tooling, is always longer than the desktop rollout suggests.
How Celvex catches this
Find. Prove. Fix. Verify.
We inventory the Git client version everywhere a clone runs, developer endpoints, CI runners, and base images, and cross-reference each against the fixed release for its line, surfacing every pre-fix binary that touches untrusted code.
A confirmed exposed client becomes an Ed25519-signed Proof Capsule carrying the host, the exact Git version, and the matching advisory and catalog entry, reproducible offline by you or your auditor.
The capsule's remediation block names the steps: upgrade to the fixed build, add the CI version gate, and contain recursive clones of untrusted repositories behind inspection.
A fresh sweep confirms the client is patched and the version gate holds. The finding closes and the verified-fix event is recorded for the audit trail.
The reason a one-byte bug in Git reaches so far is that Git sits at a trust boundary nobody labels as one. We think of a clone as reading, not running, so the scrutiny we apply to executing untrusted code is quietly skipped for the act of fetching it. CVE-2025-48384 is the reminder that on an unpatched client, fetching is running. The discipline is to treat every clone of code you do not own as the execution event it can be: patched client, gated version, inspected submodules. Answer that, and a carriage return stops being a foothold.
Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.
Sources
- Datadog Security Labs: Git arbitrary file write (CVE-2025-48384)
- dgl.cx: Git clone submodule carriage-return vulnerability (CVE-2025-48384)
- Help Net Security: Git vulnerability CVE-2025-48384 exploited in the wild
- NVD: CVE-2025-48384
- MITRE CWE-59: Improper Link Resolution Before File Access
- CISA Known Exploited Vulnerabilities Catalog
- CELVEX Group: Proof Capsule format
Which Git clients on your fleet still write the bad byte?
Free Exposure Check, no signup required. We inventory the Git version everywhere a clone runs, from developer laptops to CI base images, cross-reference each against the fixed releases, and ship a signed Proof Capsule for the highest-confidence finding.
Run a Free Scan →