Back to Blog
critical SEVERITY8 min read

How Remote Code Execution via Security Fix Bypass happens in Node.js and how to fix it

CVE-2026-28292 is a critical Remote Code Execution vulnerability in the `simple-git` Node.js library that allowed attackers to bypass previously applied security fixes. Applications using `simple-git` versions below 3.32.3 remained exposed even after earlier patches, and upgrading to 3.32.3 — which introduced hardened argument parsing via new `@simple-git/argv-parser` and `@simple-git/args-pathspec` sub-packages — closes the bypass. This fix is especially urgent because the vulnerability affects

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-28292 is a critical Remote Code Execution (RCE) vulnerability in the `simple-git` Node.js library (CWE-78: OS Command Injection) that bypasses prior security patches present in versions up to 3.27.0. Attackers can supply crafted Git arguments or pathspec values that escape sanitization logic introduced in earlier fixes, ultimately executing arbitrary OS commands on the host. The fix is to upgrade `simple-git` from `^3.27.0` to `^3.32.3` in `package.json`, which introduces two new hardened sub-packages — `@simple-git/argv-parser@1.1.1` and `@simple-git/args-pathspec@1.0.3` — that correctly validate and isolate argument construction before any shell interaction.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade simple-git from 3.27.0 to 3.32.3, which introduces dedicated hardened argument parsing packages
riskArbitrary command execution on the host system running the application
languageJavaScript / Node.js
root causePrior security patches in simple-git's argument/pathspec handling could be bypassed by specially crafted input, reaching the underlying Git subprocess
vulnerabilityRemote Code Execution via argument injection bypass

The Danger of "Already Fixed" Vulnerabilities

There's a particularly insidious class of security vulnerability that is easy to overlook: the bypass of a prior fix. When a library patches a known vulnerability, developers often update once and consider the matter closed. CVE-2026-28292 is a sharp reminder that this assumption can be dangerously wrong.

In this codebase, the yarn.lock file pinned simple-git at ^3.27.0 — a version that had already received earlier security patches. Yet Trivy's static analysis flagged it as critically vulnerable. The reason: attackers had found a way to bypass those earlier mitigations, and the only real protection was upgrading to simple-git@3.32.3, which ships a fundamentally redesigned argument-handling architecture.


The Vulnerability Explained

What is simple-git and why does it matter here?

simple-git is one of the most widely used Node.js wrappers around the Git command-line tool. It allows applications to programmatically run Git operations — cloning repositories, reading logs, checking out branches — by constructing and executing Git subprocess calls under the hood.

The critical word is subprocess. Every time simple-git runs a Git command, it builds a string or array of arguments and hands them to the OS. If any part of those arguments can be influenced by user-controlled input and the sanitization logic is incomplete, an attacker can inject additional shell commands.

The bypass: why 3.27.0 was still vulnerable

Earlier CVEs against simple-git (such as CVE-2022-25912 and CVE-2022-24066) were patched by adding validation logic directly inside the library's core argument construction code. However, CVE-2026-28292 demonstrates that this validation could be bypassed — likely through edge cases in how pathspec arguments or argv values were parsed before reaching the sanitization layer.

The vulnerable dependency declaration in package.json was:

// BEFORE (vulnerable)
"simple-git": "^3.27.0"

And in yarn.lock, the resolved package had no @simple-git/argv-parser or @simple-git/args-pathspec sub-packages — meaning argument construction and pathspec handling were handled by inline logic that retained the exploitable bypass.

A concrete attack scenario

Consider an application that uses simple-git to clone or inspect a user-supplied repository URL or branch name:

const git = simpleGit('/app/workspace');
// userBranch comes from an HTTP request parameter
await git.checkout(userBranch);

With simple-git@3.27.0, an attacker could craft a userBranch value that looks superficially like a valid branch name but contains characters or sequences that slip past the existing validation — for example:

main --upload-pack=touch${IFS}/tmp/pwned

Because the prior fix's sanitization did not account for this specific bypass pattern, the crafted value would be passed through to the underlying Git subprocess, resulting in arbitrary command execution on the host. In a CI/CD pipeline, a code review tool, or any service that runs Git operations on user-provided input, this translates directly to full server compromise.

Real-world impact

This vulnerability affects production code — not test utilities. The package.json change confirms simple-git is a runtime dependency. Any service that:

  • Accepts repository URLs, branch names, commit hashes, or file paths from users
  • Runs Git operations in response to webhooks or API calls
  • Uses simple-git in a CI/CD, code analysis, or developer tooling context

...is potentially exposed to complete host compromise via remote code execution.


The Fix

What changed: a new argument-handling architecture

The upgrade from 3.27.0 to 3.32.3 is not just a version bump — it introduces two new dedicated sub-packages that take over the responsibility of argument construction:

@simple-git/argv-parser@1.1.1 — a standalone parser for Git command-line arguments, with its own dependency on @simple-git/args-pathspec:

# yarn.lock — AFTER fix
"@simple-git/argv-parser@^1.1.0":
  version "1.1.1"
  resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/..."
  integrity sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==
  dependencies:
    "@simple-git/args-pathspec" "^1.0.3"

"@simple-git/args-pathspec@^1.0.3":
  version "1.0.3"
  resolved "https://registry.yarnpkg.com/@simple-git/args-pathspec/..."
  integrity sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==

@simple-git/args-pathspec@1.0.3 — a dedicated module for pathspec argument handling, isolating the exact vector that was previously bypassable.

Before and after

// package.json — BEFORE
"simple-git": "^3.27.0"

// package.json — AFTER
"simple-git": "^3.32.3"

The yarn.lock diff also shows a split in the debug dependency resolution — debug@^4.3.5 was previously resolved alongside debug@^4.1.1 in a single block, but the new version correctly separates debug@^4.4.0 as a distinct resolution:

# BEFORE — shared resolution, potentially pulling in older debug
debug@^4.1.1, debug@^4.3.5:
  version "4.4.0"

# AFTER — clean separation
debug@^4.1.1:
  version "4.4.0"

debug@^4.4.0:
  version "4.4.3"

This separation matters because it ensures each package gets the exact debug version it was tested against, reducing the risk of subtle behavioral differences in error logging that could mask injection attempts.

Why this architecture change closes the bypass

By extracting argument parsing into @simple-git/argv-parser and pathspec handling into @simple-git/args-pathspec, the simple-git maintainers created a clear, auditable boundary between "user-supplied input" and "arguments passed to the Git subprocess." Each sub-package enforces its own strict validation rules, and the bypass vector — which relied on edge cases in the monolithic validation logic — no longer has a path to the subprocess call.


Key Takeaways

  • Prior patches are not permanent protection: simple-git@3.27.0 had already received security fixes, yet CVE-2026-28292 bypassed them entirely. Version 3.32.3 with its new @simple-git/argv-parser architecture is required for real protection.
  • The yarn.lock file is a security artifact: Trivy detected this vulnerability by scanning yarn.lock, not just package.json. Always include lock files in your security scanning pipeline.
  • Architectural fixes beat patch fixes: The introduction of dedicated @simple-git/argv-parser and @simple-git/args-pathspec sub-packages represents a structural improvement, not just another inline patch — making future bypasses significantly harder.
  • User-controlled input + subprocess = highest risk: Any code path where user input flows into a Git operation (branch names, URLs, pathspecs, commit hashes) must be treated as a critical injection risk, regardless of library version.
  • debug dependency splitting matters: The yarn.lock change that separates debug@^4.4.0 from debug@^4.1.1 is a signal that the new version's dependency graph is cleaner and better isolated — a positive indicator of overall package hygiene.

How Orbis AppSec Detected This

  • Source: User-influenced input passed to simple-git API methods (e.g., branch names, pathspecs, repository URLs) originating from HTTP request parameters or external data sources.
  • Sink: The underlying Git subprocess invocation within simple-git's argument construction logic, where crafted input could reach the OS command layer — specifically the argument assembly code that @simple-git/argv-parser now replaces.
  • Missing control: The argument sanitization logic in simple-git@3.27.0 contained a bypassable pattern; the new @simple-git/argv-parser@1.1.1 and @simple-git/args-pathspec@1.0.3 sub-packages provide the missing strict boundary enforcement.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: Upgraded simple-git from ^3.27.0 to ^3.32.3 in package.json, pulling in the new hardened argument-parsing sub-packages as reflected in yarn.lock.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-28292 is a critical reminder that security is not a one-time event. A library that was patched against known RCE vectors can still harbor exploitable bypasses, and the only reliable signal that you are protected is continuous, automated scanning against up-to-date advisory databases.

The specific fix here — upgrading simple-git from 3.27.0 to 3.32.3 — does more than increment a version number. It introduces a fundamentally improved argument-handling architecture through @simple-git/argv-parser and @simple-git/args-pathspec, closing the bypass at the structural level rather than patching around it. For any Node.js application that interacts with Git repositories, especially those accepting user-supplied branch names, URLs, or file paths, this upgrade is non-negotiable.

Keep your lock files scanned, your dependencies current, and your input validation strict — even when you think you've already fixed the problem.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #872

Related Articles

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

How Command Injection happens in Node.js and how to fix it

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.

high

How command injection happens in JavaScript/Node.js and how to fix it

A build script in a Node.js library used `child_process.exec()` with template-literal-interpolated commit hashes to generate SVG diffs, creating a command injection primitive. The fix replaces `exec()` with `execFile()` and adds strict regex validation of commit hashes before they're used in any shell command.