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.


Prevention & Best Practices

1. Treat dependency upgrades as security events, not routine maintenance

CVE-2026-28292 exists specifically because a prior fix was incomplete. This means that simply having patched once is not sufficient. Implement automated dependency scanning (Trivy, Snyk, Dependabot) that continuously monitors your yarn.lock and package.json against updated advisory databases.

2. Never pass unsanitized user input to Git operations

Even with a fully patched simple-git, the principle of least privilege applies. Validate and allowlist any user-supplied values before passing them to Git operations:

// Dangerous — user input directly to git
await git.checkout(req.body.branch);

// Safer — validate against known-safe pattern first
const SAFE_BRANCH = /^[a-zA-Z0-9._\-\/]{1,200}$/;
if (!SAFE_BRANCH.test(req.body.branch)) {
  throw new Error('Invalid branch name');
}
await git.checkout(req.body.branch);

3. Use software composition analysis (SCA) in CI/CD

The Trivy rule CVE-2026-28292 caught this by matching the version range in yarn.lock. Integrate SCA scanning as a required CI step so vulnerable dependencies are blocked before they reach production:

# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

4. Lock and audit transitive dependencies

The new @simple-git/argv-parser and @simple-git/args-pathspec sub-packages appear in yarn.lock as explicit resolved entries. Regularly audit your lock file — not just your direct dependencies — for unexpected or vulnerable transitive packages.

5. Relevant security standards

  • OWASP A03:2021 – Injection: This vulnerability is a direct instance of injection via OS command construction.
  • CWE-78: Improper Neutralization of Special Elements used in an OS Command.
  • OWASP Dependency-Check / OWASP SCA guidance: Always run composition analysis against both package.json and lock files.

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.


References

Frequently Asked Questions

What is a security fix bypass vulnerability?

A security fix bypass occurs when a patch intended to close a vulnerability is incomplete or can be circumvented by slightly different malicious input, leaving the underlying attack vector still reachable.

How do you prevent argument injection in Node.js Git libraries?

Always upgrade to the latest patched version of the library, avoid passing unsanitized user input to Git operations, and use dedicated argument-parsing packages that enforce strict input validation before constructing subprocess calls.

What CWE is this RCE vulnerability?

This vulnerability maps to CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'), because crafted input can escape argument boundaries and reach the underlying Git process as executable commands.

Is pinning to a prior "safe" version of simple-git enough to prevent this?

No. CVE-2026-28292 specifically bypasses fixes that were present in earlier versions, so only upgrading to 3.32.3 or later — which ships the new hardened argv-parser — provides reliable protection.

Can static analysis detect this vulnerability?

Yes. Trivy flagged this exact pattern by matching the vulnerable version range in yarn.lock against its advisory database, demonstrating that software composition analysis (SCA) tools are effective at surfacing transitive and direct dependency vulnerabilities like this one.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #872

Related Articles

high

How Shell Injection via os.system() happens in Python and how to fix it

A shell injection vulnerability in TensorFlow's DELF dataset download script allowed attackers who controlled the `data_dir` parameter to execute arbitrary shell commands by injecting metacharacters into `os.system()` calls. The fix replaces all four `os.system()` invocations with `subprocess.run()` using argument lists, eliminating shell interpretation entirely. This change closes a high-severity code execution path in production ML infrastructure.

critical

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

CVE-2026-9277 is a critical command injection vulnerability in the `shell-quote` npm package (versions prior to 1.8.4) caused by unescaped line terminators that allow attackers to inject and execute arbitrary shell commands. The fix pins `shell-quote` to `>=1.8.4` via a `pnpm.overrides` entry, ensuring every transitive consumer in the dependency tree receives the patched version. Any Node.js project that processes user-influenced input through `shell-quote` and has not yet upgraded is at risk of

critical

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

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote 1.8.3, where unescaped line terminators in parsed shell arguments could allow attackers to inject and execute arbitrary commands. The fix upgrades shell-quote to version 1.8.4 and pins the resolution in both `package.json` and `yarn.lock` to ensure the patched version is used across the entire dependency tree. Because this package is used in a production web application that processes user-influenced input,

critical

How Command Injection happens in Python PopClip Extensions and how to fix it

A critical command injection vulnerability was discovered in `contrib/Klipz.popclipext/Klipz.py`, where user-controlled clipboard content was concatenated directly into shell commands executed via `osascript`. The fix replaces unsafe string concatenation with `subprocess` and proper argument lists, and replaces the unsafe `pickle` serialization with `json` to eliminate a secondary deserialization risk. Together, these changes close two distinct attack surfaces in a single file.

critical

How Command Injection happens in Rust-generated Python scripts and how to fix it

A critical command injection vulnerability (CWE-78) was discovered in the Linux automation module of the `goose-mcp` crate, where Rust code generated Python scripts that passed user-controlled commands directly to `subprocess.run()` with `shell=True`. An attacker who could influence the `commands` parameter in `execute_system_script()` could inject arbitrary shell commands using metacharacters like `;`, `|`, or backticks. The fix replaces `shell=True` with `shlex.split()` and `shell=False`, and

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr