Versions of `simple-git` prior to 3.3.0 fail to sanitize input in the `fetch()` function. Attackers can inject Git flags (specifically `--upload-pack`) to achieve Remote Code Execution (RCE). The fix involves a regex blocklist against this specific flag.
A critical argument injection vulnerability in the popular `simple-git` Node.js library allows attackers to execute arbitrary system commands via the `.fetch()` method. By abusing Git's `--upload-pack` flag, malicious inputs can trick the underlying git binary into executing shell commands.
We all love wrappers. Why write twenty lines of child_process.spawn boilerplate when you can just npm install simple-git and pretend the operating system doesn't exist? It's the classic developer efficiency trap: abstracting away the underlying shell commands until you forget they are dangerous.
simple-git is a titan in the Node ecosystem. It is the go-to library for managing git repositories programmatically. CI/CD pipelines, automated deployment tools, and developer CLI utilities rely on it to handle the heavy lifting of version control. But here is the problem with abstraction: it requires trust. You trust the library to take your string, remote_repo_url, and hand it safely to the git binary.
CVE-2022-24433 breaks that trust. It turns out that simple-git was a little too simple. It took user input and concatenated it directly into the argument list for the system's git command. It’s the software equivalent of handing a loaded gun to a toddler and hoping they only aim at the target. When you control the arguments passed to a binary, you often control the execution flow of the binary itself. And in the case of Git, that flow allows for some truly spectacular fireworks.
Let's get technical. Most people hear "Command Injection" and think of semicolons and pipes (e.g., ; rm -rf /). That is Shell Command Injection, where you break out of the command context to execute a new, separate shell command.
This vulnerability is different. This is Argument Injection (CWE-88). We aren't breaking out of the git command; we are subverting it. The simple-git library constructs a command array that looks something like ['fetch', remote, branch]. If an attacker supplies a malicious remote string, it gets pushed into that array. The developer assumed the input would be a URL or a remote name (like origin).
But Git is an incredibly complex beast with hundreds of flags. If your input starts with a hyphen (-), Git interprets it as an option, not a positional argument. The "Smoking Gun" here is the --upload-pack option. This flag tells Git: "Hey, when you talk to the remote server, use this executable to handle the protocol."
If we set --upload-pack to a shell command, Git obliges and executes it. The flaw isn't that simple-git executes a shell; the flaw is that simple-git allows the user to define which executable Git uses for its internal operations.
Let's look at the crime scene. In versions prior to 3.3.0, the fetchTask function was naively trusting. It treated input as data, never suspecting it might be code.
The Vulnerable Code:
// src/lib/tasks/fetch.ts (Pre-patch)
export function fetchTask(remote: string, branch: string, customArgs: string[]): StringTask<FetchResult> {
const commands = ['fetch', ...customArgs];
// ERROR: Direct appending of user input without validation
if (remote && branch) {
commands.push(remote, branch);
}
return { commands, format: 'utf-8', parser: parseFetchResult };
}Do you see the lack of fear? The lack of cynical bounds checking? It just pushes remote and branch right onto the stack. If I pass --upload-pack=calc.exe as the remote, it goes straight to the system call.
The Fix (v3.3.0):
To fix this, the maintainers implemented a specific check for the dangerous flag. They didn't rewrite the architecture; they just added a bouncer at the door.
// src/lib/tasks/fetch.ts (Patched)
function disallowedCommand(command: string) {
// Regex to catch the specific exploit vector
return /^--upload-pack(=|$)/.test(command);
}
export function fetchTask(remote: string, branch: string, customArgs: string[]) {
const commands = ['fetch', ...customArgs];
if (remote && branch) {
commands.push(remote, branch);
}
// The new sanity check
const banned = commands.find(disallowedCommand);
if (banned) {
return configurationErrorTask(`git.fetch: potential exploit argument blocked.`);
}
// ...
}[!NOTE] This is a blocklist approach. While effective against this specific CVE, blocklists are historically fragile. If Git introduces a new dangerous flag tomorrow, this code becomes vulnerable again.
So, how do we ruin a sysadmin's day with this? We need an application that accepts a remote URL or branch name from a user and passes it to .fetch(). This is common in tools that sync repositories or check for updates.
The Attack Chain:
simple-git < 3.3.0. A GitHub bot or a CI runner is a prime candidate.id or touch a file to prove access. We format this into the --upload-pack flag.
--upload-pack="touch /tmp/pwned"git fetch --upload-pack="touch /tmp/pwned" origin.Here is how the flow looks execution-wise:
This was famously demonstrated in the HackTheBox machine "FormulaX". In that scenario, the vulnerability allowed attackers to gain an initial foothold on the server by manipulating a repository configuration feature. It turned a mundane "Check for Updates" button into a remote shell.
[!WARNING] This exploit works even if the git command fails later. The
--upload-packcommand is executed before the fetch actually completes, because Git needs to establish the transport channel first.
This is a High Severity (8.1) vulnerability for a reason. We are talking about Remote Code Execution (RCE) with the privileges of the Node.js process.
If this code is running in a Docker container, the attacker effectively owns the container. If it is running on a developer's machine (e.g., via an Electron app or a CLI tool), the attacker has access to source code, SSH keys, and valid credentials.
Because simple-git is often used in trusted environments (CI pipelines, backend automation), the privileges associated with the process are often higher than a typical web server. The attacker doesn't just get a shell; they get a shell in the heart of your infrastructure.
The remediation path is straightforward, but critical. The patch in version 3.3.0 specifically targets the Argument Injection vector used in .fetch().
Primary Fix:
npm install simple-git@latest. Ensure you are on version 3.3.0 or higher.Secondary Defense:
^[a-zA-Z0-9/_-]+$).- if it is intended to be a positional argument.It is worth noting that while this CVE covered .fetch(), subsequent vulnerabilities (CVE-2022-25860, CVE-2022-25912) found similar issues in .clone() and .pull(). This reinforces the need to keep dependencies updated aggressively. Security is a moving target, and static libraries are sitting ducks.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
simple-git steveukx | < 3.3.0 | 3.3.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-88 (Argument Injection) |
| CVSS v3.1 | 8.1 (High) |
| Attack Vector | Network (Input to .fetch) |
| Affected Component | simple-git .fetch() method |
| Key Flag | --upload-pack |
| Exploit Status | PoC Available / Verified in CTFs |
The software constructs a string for a command from trusted and untrusted data but does not properly neutralize argument delimiters, allowing the injection of new arguments.
Get the latest CVE analysis reports delivered to your inbox.