CVE-2026-102829: simple-git: `VISUAL` editor environment variable is omitted from unsafe editor detection
The `@simple-git/argv-parser` package, which simple-git uses to block unsafe Git operations, ignored the `VISUAL` environment variable when checking for editor substitution. Git falls back to `VISUAL`, so an attacker-supplied value runs an arbitrary executable during commands such as `git commit --amend`. Versions before 2.0.1 are affected. Upgrade to 2.0.1.
Published Oct 5, 2026 · Analysis Oct 25, 2026
What an attacker can do
In packages/argv-parser/src/env/parse-env.ts, the GitEnvKeys map flags editor, git_editor and git_sequence_editor as allowUnsafeEditor. prepareEnv keeps an environment entry only if its lowercased name is in that map or starts with git. VISUAL is neither, so it is dropped before any check and vulnerabilityCheck(tokens, env) returns an empty list. Git still resolves VISUAL as an editor, and it takes precedence over EDITOR.
The reporter reproduced this through simple-git's blockUnsafeOperationsPlugin with default options. EDITOR and GIT_EDITOR values threw GitPluginError. A VISUAL value did not, Git ran the attacker's script, and the commit message was rewritten.
The preconditions are specific. The application must forward attacker-influenced environment values into the Git child process, for example with simple-git's .env(). The Git command must open an editor, such as commit --amend, commit without -m, or rebase -i. No GIT_EDITOR or core.editor may override it. TERM must be something other than dumb. The attacker's executable then runs as the host user. Apps that never pass untrusted environment values to Git are not affected.
Who is affected
Affected: npm @simple-git/argv-parser before 2.0.1. The reporter verified version 1.1.1. It is used by simple-git's default unsafe-operation guard, so apps that use simple-git are exposed through it.
npm ls @simple-git/argv-parser simple-gitIn your code, look for places where request or job data becomes the env of a Git call.
@simple-git/argv-parser
- Ecosystem
- npm
- Affected
- < 2.0.1
- Fixed in
- 2.0.1
- Check
npm ls @simple-git/argv-parser
How to fix it
Upgrade @simple-git/argv-parser to 2.0.1.
npm install @simple-git/[email protected]The fix pull request adds VISUAL to the variables tracked for allowUnsafeEditor. The sources do not say which simple-git release pulls in the fixed parser, so check npm ls after upgrading and force the version through an override if needed. Until then, do not forward untrusted environment values to Git, and set a fixed GIT_EDITOR or core.editor.
How to tell if you were hit
Look for VISUAL in the environment of Git child processes started by your application, and for editor executables you do not recognise being launched by git. Check for unexpected rewritten commit messages or .git/COMMIT_EDITMSG and .git/rebase-merge/git-rebase-todo access by other binaries. Review where in your code a request field can become an environment variable.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Security control bypass in @simple-git/argv-parser: Git's VISUAL editor fallback is not classified as allowUnsafeEditor
Report metadata
| Field | Value | | Package | `@simple-git/argv-parser` (npm, `pkg:npm/%40simple-git/argv-parser`) | | Repository | https://github.com/steveukx/git-js | | Component | `parseEnv` (`packages/argv-parser/src/env/parse-env.ts`), reached via `vulnerabilityCheck(tokens, env)` | | Vulnerability class | Security-control bypass — incomplete denylist in unsafe-editor detection | | Verified against | `c427fbad33f1f2b11341f1cf852eedecbb106400` (`@simple-git/argv-parser` 1.1.1), plus the published npm artifact 1.1.1; `main` at `98864c6` observed still unpatched | | API surface | Public documented API (`parseEnv(raw)` / `vulnerabilityCheck(tokens, env)`) | | Affected in default configuration | Yes — reproduced with `blockUnsafeOperationsPlugin` under default options, with no unsafe allowances enabled |
Summary
GitEnvKeys in packages/argv-parser/src/env/parse-env.ts maps only editor, git_editor and git_sequence_editor to the allowUnsafeEditor category. prepareEnv keeps an environment entry only when its lowercased name is a known GitEnvKey or starts with git, so VISUAL is discarded before collectConfigVulnerabilities ever inspects it. Git, however, falls back to VISUAL when resolving an editor, so parseEnv({ VISUAL: '/tmp/evileditor' }) reports no vulnerability while an interactive Git operation will execute that binary.
In a consuming application the shape is: environment values derived from a request or job are forwarded into the child Git environment and classified by this parser before spawn. The parser exists to classify exactly such values, and the equivalent EDITOR or GIT_EDITOR value is rejected — so the attacker gains an editor substitution that the guard is specifically designed to block.
EDITOR-> classifiedallowUnsafeEditor(GitEnvKeys, lines 5-27)GIT_EDITOR-> classifiedallowUnsafeEditor(GitEnvKeys, lines 5-27)GIT_SEQUENCE_EDITOR-> classifiedallowUnsafeEditor(GitEnvKeys, lines 5-27)VISUAL-> absent from `GitEnvKeys`; dropped by `prepareEnv`, lines 60-68 — no vulnerability emitted
Impact
A consuming application that allows attacker-influenced environment values can have an attacker-selected executable launched by Git during operations such as git commit --amend, bypassing the parser's default unsafe-editor protection. Execution happens as the host user running the Git child process, with the attacker's binary invoked against the repository's editor file (for example .git/COMMIT_EDITMSG, or .git/rebase-merge/git-rebase-todo for git rebase -i).
The new capability is the bypass itself: without this gap, the same attacker-supplied value under EDITOR, GIT_EDITOR or GIT_SEQUENCE_EDITOR is refused unless the consumer explicitly opts in to allowUnsafeEditor. With VISUAL, the equivalent code execution proceeds with no opt-in and no reported vulnerability. VISUAL also takes precedence over EDITOR, the variable the parser does flag.
Scoring note: no CVSS vector or score is available for this finding, and one is not asserted here. Exploitability depends on the consuming application's data flow — specifically whether attacker-influenced environment entries reach the Git child environment. Consumers that never forward untrusted environment values into Git, or that always set a higher-priority GIT_EDITOR or core.editor, are not affected.
Preconditions
- The consumer forwards attacker-influenced environment entries into the environment passed to child Git (for example via simple-git's
.env()), and classifies them with this parser before spawn. - The Git command opens an editor — for example
commitwithout-m,commit --amend, orrebase -i. - No higher-priority editor setting overrides
VISUAL:GIT_EDITOR,core.editorandEDITORare absent (GIT_EDITORandcore.editortake precedence;VISUALitself overridesEDITOR). TERMis set to a value other than exactlydumb— Git consultsVISUALonly then.TERMis neither aGitEnvKeynorgit-prefixed, so an attacker who controls the environment object supplies it too and the parser reports nothing for it either.- The attacker-selected executable exists and is runnable on the host.
This requires no non-standard usage, no monkey-patching and no unusual configuration: the affected path is the documented, default-enabled guard. docs/PLUGIN-UNSAFE-ACTIONS.md ("Text editor") documents this control as covering editor environment variables that substitute an arbitrary binary, but lists only EDITOR, GIT_EDITOR and GIT_SEQUENCE_EDITOR; Git's VISUAL fallback is not mentioned, and the string visual does not appear anywhere in the repository at the verified commit. The docs do state that supplying environment values is the caller's responsibility, but they do not warn that VISUAL is outside the guard.
The feed payload's precondition list also carries entries relating to a separate GIT_CONFIG_PARAMETERS / allowUnsafeConfigEnvCount config-injection scenario. Those were not needed here: the bypass was reproduced with default options and no unsafe allowances enabled.
Data flow
- Source — the attacker-influenced
rawenvironment object enters the public parser entry point:parseEnv(raw)(`packages/argv-parser/src/env/parse-env.ts:70`). - Propagation —
prepareEnvlowercases keys and retains only knownGitEnvKeysor names starting withgit;visualis neither, so the entry is dropped before any analysis sees it (`packages/argv-parser/src/env/parse-env.ts:60-68`). - Sink —
collectConfigVulnerabilitiestherefore never emitsallowUnsafeEditorforVISUAL, andvulnerabilityCheck(tokens, env)returns an empty list (`packages/argv-parser/src/env/parse-env.ts:45-54`). - Reachability — simple-git's
blockUnsafeOperationsPluginpasses its environment intovulnerabilityCheckbefore spawn; an empty vulnerability list means the Git child process is allowed to start (`simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts:12-20`).
Vulnerable code
packages/argv-parser/src/env/parse-env.ts, lines 5-27 at c427fbad33f1f2b11341f1cf852eedecbb106400:
const GitEnvKeys = {
'editor': 'allowUnsafeEditor',
// ...
'git_editor': 'allowUnsafeEditor',
// ...
'git_sequence_editor': 'allowUnsafeEditor',
// VISUAL missing
} as const satisfies Record<string, VulnerabilityCategory>;The three mapped keys are the safe siblings; the missing visual entry is the gap. Because prepareEnv (lines 60-68) filters on this map plus a git prefix, the omission is not merely a missing classification — the value never reaches the classifier at all.
Reproduction
Verified — reproduced dynamically, end to end, against a checkout of c427fbad33f1f2b11341f1cf852eedecbb106400 (packages/argv-parser/package.json = 1.1.1) and against the published npm artifact 1.1.1.
Observed at the parser level (vitest PoC run against the repo):
parseEnv({ EDITOR }),parseEnv({ GIT_EDITOR })andparseEnv({ GIT_SEQUENCE_EDITOR })each yield oneallowUnsafeEditorvulnerability.parseEnv({ VISUAL: '/tmp/poc/evileditor' })yields[]in every casing.vulnerabilityCheck(['commit', '--amend'], { VISUAL })— the exact call the spawn guard makes — returns[].- The published
dist/index.cjsof 1.1.1 contains zero occurrences ofvisual; the same holds for the repository at the verified commit, includingdocs/PLUGIN-UNSAFE-ACTIONS.md.
Observed at the Git level (only VISUAL set, EDITOR and GIT_EDITOR unset): git var GIT_EDITOR returned the attacker path; git commit --amend executed the attacker script (marker written, commit subject rewritten); git rebase -i executed it for the rebase-todo as well. VISUAL also took precedence over EDITOR (EDITOR=/bin/true VISUAL=evil -> evil).
Observed end to end through the real spawn path (simple-git built from this commit, default options, no unsafe allowances): the EDITOR and GIT_EDITOR variants both threw GitPluginError — "Use of ... is not permitted without enabling allowUnsafeEditor" — with no execution. The VISUAL variant was not blocked: the plugin saw an empty vulnerability list, Git spawned, and the attacker-supplied editor executed as the host user against .git/COMMIT_EDITMSG, rewriting the commit message.
Minimal Git-level reproduction:
#!/bin/sh
set -e
rm -rf /tmp/visual-repo /tmp/GIT_VISUAL_POC /tmp/evileditor
mkdir /tmp/visual-repo && cd /tmp/visual-repo
git init -q
git config user.email a@a && git config user.name a
touch a && git add a && git commit -qm init
printf '#!/bin/sh\ntouch /tmp/GIT_VISUAL_POC\nexit 1\n' >/tmp/evileditor
chmod +x /tmp/evileditor
env -u EDITOR -u GIT_EDITOR VISUAL=/tmp/evileditor git commit --amend || true
test -e /tmp/GIT_VISUAL_POC && echo executedGuard-level reproduction, using the public API:
import { vulnerabilityCheck } from '@simple-git/argv-parser';
import { spawnSync } from 'node:child_process';
const args = ['commit', '--amend'];
const env = { VISUAL: '/tmp/evileditor' };
if (vulnerabilityCheck(args, env).length === 0) spawnSync('git', args, { env });- Expected:
parseEnv({ VISUAL: '/tmp/evileditor' }).vulnerabilitiescontainsallowUnsafeEditor, and the spawn guard refuses the operation unless the consumer has enabledallowUnsafeEditor— the behaviour already applied toEDITOR,GIT_EDITORandGIT_SEQUENCE_EDITOR. - Actual: no vulnerability is reported, the guard permits the spawn, and Git executes the attacker-selected binary.
Suggested remediation
Treat VISUAL as an editor source, so Git's own editor-resolution precedence is fully covered by the denylist.
const GitEnvKeys = {
'editor': 'allowUnsafeEditor',
'visual': 'allowUnsafeEditor',
// ...
'git_editor': 'allowUnsafeEditor',
// ...
'git_sequence_editor': 'allowUnsafeEditor',
} as const satisfies Record<string, VulnerabilityCategory>;Because prepareEnv filters on GitEnvKeys membership, this single entry is enough to make visual survive filtering and be classified; no change to prepareEnv or collectConfigVulnerabilities is required.
Notes:
TERMis likewise neither aGitEnvKeynorgit-prefixed, and it is the variable that decides whether Git consultsVISUALat all (TERM=dumbor unset meansVISUALis ignored). An attacker who controls the environment object supplies it alongsideVISUAL; whetherTERMwarrants its own classification is a maintainer judgement call, but it is worth considering while fixing this.git rebase -ireaches the same sink:git_sequence_editorfalls back to normal editor resolution, so theVISUALpath executes the attacker binary against the rebase-todo file as well.docs/PLUGIN-UNSAFE-ACTIONS.md("Text editor") should listVISUALalongsideEDITOR/GIT_EDITOR/GIT_SEQUENCE_EDITOR, since the documented scope of the control is what consumers rely on.- Already safe and needing no change:
EDITOR,GIT_EDITORandGIT_SEQUENCE_EDITORare all correctly classified and enforced by the plugin, and the parser's handling ofgit-prefixed variables is unaffected.
Suggested regression test alongside test/parse-env.spec.ts (which currently covers EDITOR / GIT_EDITOR / GIT_SEQUENCE_EDITOR / PAGER but has no VISUAL case): assert that parseEnv({ VISUAL: '/tmp/evileditor' }) yields one allowUnsafeEditor vulnerability in every casing, and that vulnerabilityCheck(['commit', '--amend'], { VISUAL: '/tmp/evileditor' }) returns that vulnerability rather than []. A precedence case is worth adding too: VISUAL set together with EDITOR must still be flagged, since VISUAL wins in Git's resolution order.
Questions
- Does this affect all simple-git users?
No. The app must pass attacker-influenced environment values into the Git child process, and run a Git command that opens an editor. Apps that never do either are not affected.
- Why are EDITOR and GIT_EDITOR blocked but not VISUAL?
The parser's list of tracked variables included
editor,git_editorandgit_sequence_editorbut omittedvisual. The omitted variable is dropped before any check, and Git still honors it.- Which version fixes CVE-2026-102829?
@simple-git/argv-parser2.0.1. The sources do not state which simple-git release depends on it.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
