How Remote Code Execution via Security Fix Bypass Happens in Node.js and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-28292 |
| Severity | Critical |
| Package | simple-git (npm) |
| Affected version | < 3.32.3 (project was on 3.30.0) |
| Fixed version | 3.36.0 |
| CWE | CWE-78 – OS Command Injection |
| Impact | Remote Code Execution |
Introduction
The package-lock.json file in the nomacode project pinned simple-git at ^3.22.0, which resolved to version 3.30.0 at install time. That version carries CVE-2026-28292 — a critical Remote Code Execution vulnerability that is particularly dangerous because it is not a brand-new attack class: it is a bypass of security fixes that were already shipped in earlier versions of the library.
Bypass vulnerabilities are some of the most insidious in the ecosystem. Developers who saw previous simple-git CVEs, upgraded, and moved on may believe they are protected. CVE-2026-28292 proves they are not — at least not until version 3.32.3.
The nomacode project uses simple-git alongside express and ws, strongly suggesting it exposes a web interface or WebSocket API that can trigger git operations. If any of those operations accept user-controlled input — a branch name, a commit ref, a path — the application was vulnerable to full remote code execution on the server.
The Vulnerability Explained
What simple-git does
simple-git is a lightweight Node.js wrapper around the git command-line tool. It spawns child processes that call git with arguments constructed from the values you pass to its API. For example:
// Typical usage in a web application
const git = simpleGit('/path/to/repo');
// If branchName comes from an HTTP request parameter...
app.post('/checkout', async (req, res) => {
const branchName = req.body.branch; // ← user-controlled
await git.checkout(branchName); // ← passed to git subprocess
});
When branchName is main, this is perfectly safe. When branchName is something like --upload-pack=touch /tmp/pwned or a crafted pathspec that exploits git's argument parsing, the result is arbitrary command execution.
Why prior fixes were insufficient
simple-git has been patched for argument injection before. The project has a history of CVEs in this class, and each one prompted the maintainers to add sanitization for the specific bypass technique discovered. CVE-2026-28292 represents a new bypass route that circumvented those earlier controls.
The vulnerable version in this project was 3.30.0:
// package-lock.json BEFORE fix
"node_modules/simple-git": {
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
"integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="
}
Version 3.30.0 did not include the architectural refactor that properly isolates argument construction from user input. An attacker who could influence any string passed to a simple-git method — a branch name, a tag, a remote URL, a file path — could inject additional git arguments or shell metacharacters that would be forwarded to the underlying git subprocess.
Concrete attack scenario for nomacode
Given that nomacode uses express and ws, consider a WebSocket handler like:
// Hypothetical handler in nomacode
ws.on('message', async (message) => {
const { action, ref } = JSON.parse(message);
if (action === 'fetch') {
await git.fetch('origin', ref); // ref is attacker-controlled
}
});
With simple-git 3.30.0, a malicious WebSocket client could send:
{
"action": "fetch",
"ref": "--upload-pack=id>/tmp/rce_proof origin main"
}
The bypass in CVE-2026-28292 means the sanitization that should have blocked this specific pattern did not. The git fetch subprocess would receive the injected --upload-pack flag, executing the attacker's command (id>/tmp/rce_proof) on the server.
Real-world impact: Full server compromise. The Node.js process running nomacode has filesystem access, environment variables (potentially containing API keys, database credentials), and network access. An RCE at this layer is a complete breach.
The Fix
Upgrading simple-git to 3.36.0
The fix is a version upgrade in both package.json and package-lock.json:
// package-lock.json BEFORE
"simple-git": "^3.22.0" // resolves to 3.30.0
// package-lock.json AFTER
"simple-git": "^3.36.0" // resolves to 3.36.0
// node_modules/simple-git BEFORE
{
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
"integrity": "sha512-q6lxyDsCmEal/..."
}
// node_modules/simple-git AFTER
{
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz",
"integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/Io..."
}
The architectural change: dedicated argument-parsing sub-packages
The most significant aspect of the fix visible in the diff is the introduction of two new sub-packages that did not exist in 3.30.0:
// NEW in the fixed package-lock.json
"node_modules/@simple-git/args-pathspec": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz",
"integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==",
"license": "MIT"
},
"node_modules/@simple-git/argv-parser": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz",
"integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==",
"license": "MIT",
"dependencies": {
"@simple-git/args-pathspec": "^1.0.3"
}
}
This is not just a patch on top of existing code. The maintainers extracted argument parsing into purpose-built, independently versioned packages:
@simple-git/argv-parser— handles the construction and validation of command-line arguments passed to git. By isolating this logic, the team can apply strict allow-listing and boundary enforcement in one place, rather than patching individual call sites.@simple-git/args-pathspec— specifically handles pathspec arguments (file paths, globs, refs), which are a historically common injection vector in git tooling. Pathspecs have their own syntax rules and their own injection risks; a dedicated module means those rules are enforced consistently.
Why this matters: Previous patches for similar CVEs in simple-git were applied reactively — a specific bypass technique was found, a specific check was added. The refactor into @simple-git/argv-parser and @simple-git/args-pathspec represents a proactive, structural defense: all arguments flow through validated parsers regardless of which simple-git method is called.
Before vs. after: what changed for developers
No API changes are required in nomacode. The upgrade is entirely in the dependency layer. The same git.checkout(branchName) call now routes through @simple-git/argv-parser, which validates and sanitizes branchName before it ever reaches the subprocess invocation.
Prevention & Best Practices
1. Keep dependencies pinned to exact versions in production
The ^3.22.0 range specifier in package.json allowed npm to install 3.30.0, which was vulnerable. Consider using exact version pins ("simple-git": "3.36.0") in production and automating upgrades through a tool like Dependabot or Renovate.
2. Validate user input before passing it to git operations
Even with a patched library, defense in depth requires validating input at the application layer:
// Example: validate branch names before passing to simple-git
const SAFE_REF_PATTERN = /^[a-zA-Z0-9._\-\/]+$/;
app.post('/checkout', async (req, res) => {
const branchName = req.body.branch;
// Reject anything that doesn't look like a valid git ref
if (!SAFE_REF_PATTERN.test(branchName)) {
return res.status(400).json({ error: 'Invalid branch name' });
}
await git.checkout(branchName);
});
3. Run vulnerability scanners in CI
Trivy detected this vulnerability by comparing the installed version in package-lock.json against its CVE database. Integrate Trivy or a similar scanner (Snyk, npm audit, OWASP Dependency-Check) into your CI pipeline so vulnerable dependencies are caught 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. Treat git library inputs as untrusted
Any string that originates outside your application — HTTP parameters, WebSocket messages, environment variables set by external systems — must be treated as untrusted. This is especially true for inputs that will be passed to subprocess-wrapping libraries like simple-git.
5. Monitor the simple-git security advisory history
simple-git has had multiple CVEs in the argument injection class. Subscribe to GitHub security advisories for this package and review the npm advisory database regularly.
Relevant standards
- OWASP A03:2021 – Injection: This vulnerability falls squarely in the injection category. The OWASP OS Command Injection Defense Cheat Sheet recommends avoiding shell invocations entirely where possible, and using parameterized APIs when they are necessary.
- CWE-78: Improper Neutralization of Special Elements used in an OS Command. The mitigation guidance recommends input validation, output encoding, and the principle of least privilege for the process executing commands.
Key Takeaways
simple-git3.30.0 inpackage-lock.jsonwas the direct source of the risk — the version range^3.22.0silently allowed a vulnerable install. Pinning or automating upgrades would have caught this sooner.- Bypass vulnerabilities are more dangerous than new CVEs — developers who patched earlier simple-git CVEs may have believed they were safe. CVE-2026-28292 demonstrates that incremental patches on argument injection are fragile; the architectural refactor in 3.36.0 is a more durable fix.
- The introduction of
@simple-git/argv-parserand@simple-git/args-pathspecis the real fix — not just a version bump. These new sub-packages centralize and harden all argument validation in one auditable place. - nomacode's use of
expressandwsmeans user input can reach git operations over HTTP or WebSocket — any user-facing endpoint that triggers a git call must validate its inputs independently of the library. - Trivy caught this at the
package-lock.jsonlevel — static analysis on the lock file, not justpackage.json, is essential because lock files reflect what is actually installed.
How Orbis AppSec Detected This
- Source: User-controlled strings (e.g., branch names, refs, paths) entering the application via HTTP request parameters or WebSocket messages in the
nomacodeapplication. - Sink: The
simple-gitlibrary's internal subprocess invocation, where user-controlled strings were assembled into git command arguments without complete sanitization — reachable through anygit.*()call in the application code. - Missing control: The installed version of
simple-git(3.30.0) lacked the@simple-git/argv-parserand@simple-git/args-pathspecsub-packages that enforce strict argument validation; earlier sanitization logic contained a bypass that CVE-2026-28292 exploits. - CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command (OS Command Injection).
- Fix: Upgraded
simple-gitfrom3.30.0to3.36.0inpackage-lock.jsonand updated the version range inpackage.jsonfrom^3.22.0to^3.36.0, introducing the hardened@simple-git/argv-parserarchitecture.
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 stark reminder that patching a vulnerability class once is not enough. simple-git has been through this rodeo before — and each time, the patches were applied to specific bypass techniques rather than the underlying architectural problem. Version 3.36.0 changes that by extracting argument handling into dedicated, independently versioned packages (@simple-git/argv-parser, @simple-git/args-pathspec) that enforce validation at the boundary between application code and the git subprocess.
For the nomacode project, the upgrade from 3.30.0 to 3.36.0 closes the RCE vector. But the broader lesson is architectural: any Node.js application that passes user-influenced data to a subprocess-wrapping library must treat that data as untrusted, validate it at the application layer, and keep the underlying library current. Dependency scanning in CI — as demonstrated by Trivy's detection of this exact version in package-lock.json — is the safety net that catches what code review misses.