Vibe-TradingのFastAPIエンドポイントは、認証なしでアクセス可能であり、ファイルアップロードが可能で、リモートコード実行(RCE)の脆弱性につながる可能性があります。

### 概要:
5つの脆弱性が見つかりました。
- 認証なしでAPI全体が公開されている(F1、重大)。
- `API_AUTH_KEY`を設定しても、読み取り側の認可のギャップが残っている(F2)。
- 認証なしで、`.py`、`.sh`、`.yaml`ファイルがサーバーから返されるパスに書き込まれる(F3)。
- デフォルトでは許可的なCORS設定があり、ローカルホストの許可されたポートでアクセスするすべてのブラウザページに対して、認証されたクロスオリジンアクセスが許可される(F-A4)。
- `_mask_secret()`関数を介して、APIキーの一部が公開される(F-A5)。

---

### 共通の前提条件(5つの脆弱性すべてに適用)

同梱されている`agent/.env.example`ファイルの112行目には、`# API_AUTH_KEY=`というコメントがあります。`agent/api_server.py`ファイルの303行目の`require_auth()`関数は、`API_AUTH_KEY`が設定されていない場合、`if not api_key: return`が実行され、すぐに`None`を返します。そのため、`dependencies=[Depends(require_auth)]`で装飾されたすべてのエンドポイントは、認証なしで動作します。同梱されている`Dockerfile`には、`USER`ディレクティブが含まれていません。そのため、FastAPIプロセスはコンテナ内で`uid=0(root)`として実行されます(確認済み:`docker exec id`コマンドを実行すると、`uid=0(root) gid=0(root)`と表示されます)。`docker-compose.yml`ファイルでは、ネットワーク制限なしで`0.0.0.0:8899`がバインドされています。

クリーンインストール以外に必要な操作は、エージェントがツール呼び出しのラウンドトリップを完了できるように、有効なLLM APIキーを提供することです。これは、エージェントを機能させるための通常の最初のステップであり、追加のセキュリティオプションではありません。F-A4とF-A5は、LLMキーを必要としません(各脆弱性に関する注記を参照)。F1とF3は、認証なしの脆弱性自体にはLLMキーを必要としませんが、F1で示されているRCEのデモンストレーションには必要です。

### 再現環境(共通)

```sh
git clone https://github.com/HKUDS/Vibe-Trading.git
cd Vibe-Trading
git checkout 7452610113a75529b5d55fd2217bb17f7bec66f7 # v0.1.6 + フロントエンドの修正; v0.1.6 と同じ脆弱性状態
cp agent/.env.example agent/.env
# (F1 チェーンデモの場合のみ:) agent/.env を編集して、OPENROUTER_API_KEY=<実際のキー> を設定します。
docker compose up -d
# ポート 8899 が利用可能になりました。HOST は、攻撃者から見たときの Docker ホストの IP アドレスです。
```

> **以下の各脆弱性に関する「確認方法」セクションで使用されている `HOST` のプレースホルダーに関する注意**: `HOST` を、Docker ホストにアクセスできるアドレスに置き換えてください。通常は、コンテナと同じマシンで再現環境を実行している場合は `localhost` (または `127.0.0.1`) になります。以下のすべての `curl` コマンドは、この置き換えを前提としています。

---

### 脆弱性 1 — 深刻: 認証されていないネットワーククライアントが、POST /sessions/{id}/messages を介してシェル実行に到達する

- **深刻度**: 深刻
- **CVSS v3.1**: 9.8 — `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`
- **CVSS v4.0**: 10.0 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H`
- **CWE**: CWE-306 (重要な機能に対する認証の欠如); CWE-78 (グループ B、F6) に関連

**影響を受けるファイル**:
- `agent/api_server.py:303` — `if not api_key: return` `require_auth()` 内での早期リターン
- `agent/api_server.py:736` — `@app.post("/sessions/{session_id}/messages", dependencies=[Depends(require_auth)])` (API_AUTH_KEY が設定されていない場合、この依存関係は無効)
- `agent/src/tools/bash_tool.py:44-46` — `subprocess.run(command, shell=True, cwd=cwd)` コマンドは `kwargs['command']` から読み込まれる (完全なバグの詳細は GHSA-2 / グループ B / F6 で追跡中)

**意図と実際**: セッションAPIは、認証されたユーザーのみが利用することを意図しています。`API_AUTH_KEY`が設定されていない場合、`require_auth()`関数はすぐに`None`を返し、`dependencies=[Depends(require_auth)]`は動作しません。したがって、ポート8899への匿名TCPクライアントは、セッションを作成し、メッセージを送信し、LLMエージェントからの応答を受信することができます。LLM ReActエージェントは、コマンドを実行するための自然言語リクエストを受け取ると、自動的に検出されたレジストリから`BashTool`を選択し、LLMによって生成された文字列を`subprocess.run(command, shell=True)`に渡します。コンテナには`USER`ディレクティブがないため、結果として実行されるプロセスは`uid=0(root)`として実行されます。

**確認手順**:

1. 上記の共有された再現手順に従ってサーバーを起動します(`agent/.env`に有効な`OPENROUTER_API_KEY`が設定されていることを確認)。`curl -fs http://HOST:8899/health`が200を返すことを確認します。
2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")`
3. `curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Content-Type: application/json' -d '{"content":"Execute the shell command '\''id && uname -a'\'' and report the output verbatim."}'`
4. 約5〜15秒待ち、その後`curl -s "http://HOST:8899/sessions/$SID/messages"`を実行し、BashToolの結果メッセージを確認します。このメッセージには`uid=0(root)`、カーネルのバージョン、およびコンテナのホスト名が含まれており、これらはすべて、3つのリクエストすべてで`Authorization`ヘッダーなしで返されます。

**影響**: 認証されていないクライアントがポート8899へのTCPアクセスを持っている場合、コンテナ内で任意のシェルコマンドをroot権限で実行できます。これは、リモートコード実行(RCE)の最も深刻な脆弱性です。Dockerfileに`USER`ディレクティブがないため、影響範囲はコンテナ全体の制御を奪われる可能性があります。

---

### 発見事項2 — 深刻度:高 — 認証なしで、API_AUTH_KEYが設定されている場合でも、読み取りエンドポイントは完全なセッション履歴を返す。

- **深刻度**: 高
- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`
- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`
- **CWE**: CWE-862 (認証の欠如)

**影響を受けるファイル**: `agent/api_server.py` — 289行目の `require_auth()` のドキュメント文字列には「書き込みエンドポイント(POST/PUT/DELETE/PATCH)のみがこの依存関係を使用します」と記載されています。`Depends(require_auth)` がない読み取りエンドポイント:
- 804行目 — `@app.get("/runs", response_model=List[RunInfo])`
- 788行目 — `@app.get("/runs/{run_id}", response_model=RunResponse)`
- 748行目 — `@app.get("/runs/{run_id}/code")`
- 769行目 — `@app.get("/runs/{run_id}/pine")`
- 1153行目 — `@app.get("/sessions", response_model=List[SessionResponse])`
- 1173行目 — `@app.get("/sessions/{session_id}", response_model=SessionResponse)`
- 1251行目 — `@app.get("/sessions/{session_id}/messages", response_model=List[MessageResponse])`
- 1272行目 — `@app.get("/sessions/{session_id}/events")`
- 1444行目 — `@app.get("/swarm/runs")`

**意図と実際**: `API_AUTH_KEY` が設定されている場合、暗黙の期待は、すべてのセッションデータが保護されていることです。しかし、実際の設計は289行目のドキュメント文字列に記載されており、読み取りエンドポイントには `Depends(require_auth)` がないため、`API_AUTH_KEY` が設定されていても認証されません。`API_AUTH_KEY=any-secret-value` が設定された状態で実行した場合、有効なベアラー トークンでセッションが作成され、`BROKER_TOKEN=ts-secret-deadbeef-real-private-data` を含むメッセージが送信され、その後、`Authorization` ヘッダーなしで `GET /sessions/{id}/messages` が実行されました。その結果、HTTP 200 が返され、ブローカー トークンの文字列がそのままレスポンスに含まれており、認証が有効になっている場合でもこの問題が依然として存在することが確認されました。

**確認方法**:

1. `agent/.env`ファイルで、`API_AUTH_KEY=any-secret-value`を設定します。Docker Composeを使用する場合、`vibe-trading`サービスにバインドマウントを追加し、コンテナが変更を読み込めるようにします。`volumes: - ./agent/.env:/app/agent/.env:ro`。その後、`docker compose up -d --force-recreate`を実行します(単純な`restart`コマンドは、既存のプロセスの環境変数を再利用するため、変更が反映されません)。
2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")`
3. `curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{"content":"BROKER_TOKEN=ts-secret-test-value"}'` (この操作には、POSTリクエストが認証保護されているため、Bearerトークンが必要です)。
4. **認証なしでの読み取り**: `curl -s "http://HOST:8899/sessions/$SID/messages"` (Authorizationヘッダーは不要)。HTTP 200レスポンスが返され、ブローカーのトークンが表示されます。
5. また、`curl -s "http://HOST:8899/runs"`を実行すると、認証なしで、すべての実行レコード(プロンプトフィールドを含む)が返されます。

**影響**: 認証されていないユーザーが、すべてのエージェントセッションの完全な履歴を列挙できる可能性があります。これには、オペレーターがプロンプトに貼り付けたブローカーのトークン、LLM APIキー、または取引口座の詳細が含まれる場合があります。オペレーターが書き込み操作が保護されていると信じている場合でも、この脆弱性は存在し、セキュリティに関するドキュメント(SECURITY.md)に従って認証を有効にしているオペレーターにとって、欺瞞的な結果をもたらす可能性があります。

---

### 発見事項3 — 深刻度:高 — 認証なしのPOST /uploadにより、任意の.py/.sh/.yamlファイルがサーバーのファイルシステムに書き込まれ、完全なパスが返される

- **深刻度**: 高
- **CVSS v3.1**: 8.1 — `AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N`
- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:H/SI:H/SA:N`
- **CWE**: CWE-434 (危険な種類のファイルの制限のないアップロード)

**影響を受けるファイル:**
- `agent/api_server.py:1310-1321` — `_BLOCKED_UPLOAD_EXT` により、`.exe`, `.msi`, `.bat`, `.cmd`, `.com`, `.scr`, `.app`, `.dmg`, `.so`, `.dll`, `.dylib`, `.zip`, `.rar`, `.7z`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` などのファイル形式はアップロードが拒否されます。ただし、`.py`, `.sh`, `.yaml`, `.j2`, `.json`, `.html`、および `Dockerfile` は許可されます。
- `agent/api_server.py:1347` — `@app.post("/upload", dependencies=[Depends(require_auth)])` ( `API_AUTH_KEY` が設定されていない場合、何もしません)

**意図と実際:** `POST /upload` は、認証されたファイルの一時保存メカニズムとして設計されています。デフォルト設定では、認証の依存関係は機能しません (F1参照)。拡張子ブロックリストは許可リストではなく、禁止リストであるため、危険な実行ファイルに関連するファイル形式が通過する可能性があります。アップロードされたファイルは `<uuid>.<ext>` という名前で保存され、**解決されたサーバー上のフルパスがレスポンスボディの `"file_path"` の下に返される**ため、攻撃者がパスを推測する必要はありません。

**確認方法:** (LLMキーは不要)

1. デフォルト設定でサーバーを起動します ( `API_AUTH_KEY` は設定しない)。
2. `curl -s -X POST http://HOST:8899/upload -F 'file=@/dev/stdin;filename=payload.py' <<< 'print("ATTACKER_CONTROLLED")'` を実行します。
3. HTTP 200 レスポンスで、JSON に `"status":"ok"` と `"file_path":"/app/agent/uploads/<uuid>.py"` が含まれていることを確認します。
4. `filename=config.yaml` と `filename=run.sh` で上記を繰り返して、複数の実行ファイルに関連するファイル形式が通過することを確認します。

**影響:** 認証されていないユーザーは、任意のPythonスクリプト、シェルスクリプト、またはYAML設定ファイルを、サーバーが返す既知のパスに書き込むことができます。F3は、LLM APIキーに依存しません。これは、コードベースの中で最も基本的な認証なしでの書き込み機能です。アップロードされたファイルは、LLMエージェントのツール環境からアクセス可能であり、Group B / F8 (バックテスト: `exec_module` の早期実行) につながり、bashを使用しないリモートコード実行 (RCE) の経路となります。

---

### A4の脆弱性 — 中程度: デフォルトで許可的なCORS設定 + `/settings` へのループバック接続のみのチェックにより、ローカルホストで提供される任意のブラウザページが、認証されたクロスオリジンリクエストをトリガーできる。

- **深刻度**: 中
- **CVSS v3.1**: 7.7 — `AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N`
- **CVSS v4.0**: 6.3 — `AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N`
- **CWE**: CWE-942 (信頼できないドメインに対する緩いクロスドメインポリシー); CWE-346 (オリジン検証エラー)

**影響を受けるファイル**:
- `agent/api_server.py:252-264` — `_CORS_ORIGINS = os.getenv(...)` は、デフォルトで6つのローカルホストのオリジン (`http://localhost:3000`, `:5173`, `:8000` など) のリストになります (`127.0.0.1` も同様)。`CORSMiddleware` が `allow_credentials=True`, `allow_methods=["*"]`, `allow_headers=["*"]` の設定で追加されています。
- `agent/api_server.py:309-336` — `_is_local_client()` および `require_local_or_auth()`: ループバックチェックは `request.client.host` (TCPピアIP) を検証します。これは、同じマシンからの**すべての**ブラウザリクエストに対して `127.0.0.1` になります。
- `agent/api_server.py:908` — `/settings/llm` および関連するエンドポイントで `dependencies=[Depends(require_local_or_auth)]` が設定されています (ホストからのすべてのブラウザリクエストに許可されます)。

**意図と実際**: CORSの許可リストは、開発中にVite/Reactのフロントエンドが認証付きのAPI呼び出しを実行できるようにすることを目的としています。これは、開発の利便性を高めるための設定です。しかし、実際の構成では、リストされた6つのオリジンからの**すべての**ローカルWebページ(他のアプリケーション、IDEのプレビューウィンドウ、ローカルWebツール、または別のプロセスがこれらのポートで提供する静的HTMLファイルなど)が、認証付きのクロスオリジンPOST/GETリクエストを送信し、JavaScriptで応答を読み取ることができます。`_is_local_client()` でのループバックチェックは、TCPピアIPを検証します(ホストからのブラウザリクエストでは常に `127.0.0.1` になります)。そのため、許可されたオリジンからのブラウザによるクロスオリジンリクエストは、通常は `/settings/*` でのみ適用されるループバック制限を回避してしまいます。

**確認方法**: (LLMキーは不要)

1. サーバーをデフォルト設定で起動します。
2. 事前リクエスト:`curl -s -X OPTIONS "http://HOST:8899/sessions" -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -i` — レスポンスに `access-control-allow-credentials: true`, `access-control-allow-origin: http://localhost:3000`, および `access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT` が含まれていることを確認します。
3. `Origin: http://localhost:3000` を指定した実際の `POST /sessions` リクエストを実行し、レスポンスに同じ `allow-credentials` ヘッダーが含まれていることを確認します。これにより、ブラウザがリクエストボディを読み込めるようになります。
4. 設定エンドポイントに対する事前リクエスト:`curl -s -X OPTIONS "http://HOST:8899/settings/llm" -H "Origin: http://localhost:3000" -i`。次に、`curl -s "http://HOST:8899/settings/llm" -H "Origin: http://localhost:3000"` を実行し、レスポンスが許可されていることを確認します。これは、`request.client.host = 127.0.0.1` が `_is_local_client()` を満たしているためです。
5. 負のテスト:`curl -s -X OPTIONS "http://HOST:8899/sessions" -H "Origin: http://evil.example.com" -i` — CORS ヘッダーが含まれていないことを確認します。これにより、許可リストがローカルホスト以外のオリジンに対して機能していることが示されます。

**影響**: ホワイトリストに登録されたローカルホストのポートにある悪意のあるまたは侵害されたウェブページが、オペレーターのブラウザコンテキスト内で Vibe-Trading API を静かに実行する可能性があります。これにより、セッションの作成、メッセージの投稿(これにより F1 の BashTool RCE が連鎖的に発生する)、セッション履歴の読み取り(F2)、および F-A5 が公開する部分的なキーヒントを含む設定の読み取りが可能になります。必要なユーザー操作は、オペレーターがバックグラウンドで `fetch()` 呼び出しを行うページを閲覧することのみです。これにより、F1-F3 の攻撃対象領域が、直接的な TCP 攻撃者から、同じホストでエージェントを実行している開発者と共有するブラウザ経由の攻撃者にまで拡大します。

---

### A5 の発見 — 中程度:設定エンドポイントは、_mask_secret() を使用して、構成されたすべての API キーの最初の 4 文字と最後の 4 文字を公開します。

- **深刻度**: 中
- **CVSS v3.1**: 5.3 — `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N`
- **CVSS v4.0**: 6.9 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N`
- **CWE**: CWE-200 (機密情報の漏洩)

**影響を受けるファイル**:
- `agent/api_server.py:467-474` — `_mask_secret()`関数は、8文字を超える文字列の場合、`f"{value[:4]}...{value[-4:]}"`という形式で値を返す。
- `agent/api_server.py:372` — `LLM_API_KEY_PLACEHOLDERS`は、出荷時に設定されたプレースホルダーを除外するため、実際のキーが設定されている場合にのみ情報漏洩が発生する。
- `agent/api_server.py:505-522` — `GET /settings/llm`エンドポイントは、`api_key_hint = _mask_secret(api_key) if api_key_configured else None`を返す。
- `agent/api_server.py:561` — `GET /settings/data-sources`エンドポイントは、`tushare_token_hint=_mask_secret(token) if token_configured else None`を返す。
- `agent/test_settings_api.py:106` — 入力`or-secret-value`に対するアサーション`api_key_hint == "or-s...alue"`は、最初の4文字と最後の4文字が表示されることが意図されたAPI仕様であることを確認する。

**意図と実際**: フロントエンドは、APIキーが設定されていることをオペレーターに通知する必要がある。そのため、適切なヒントは、ブール値による存在を示すか、固定のプレースホルダーであるべきである。しかし、実際の実装では、最初の4文字と最後の4文字が表示される。構造化されたプロバイダーキー(`sk-or-v1-`, `gsk_`, `xoxb-`, `ts-`など、予測可能なプレフィックスを持つもの)の場合、最初の4バイトはほぼ固定であり、情報漏洩は主に最後の4バイトに集中する。これは、総エントロピーが制限されたトークン(例:Tushareトークン)にとって問題となる可能性がある。ランタイムテストでは、`OPENROUTER_API_KEY=sk-or-v1-AbCdEfGhXyZ12345fakekey`および`TUSHARE_TOKEN=ts-real-shaped-token-1234567890ab`を設定し、対応するGETエンドポイントから`api_key_hint='sk-o...ekey'`および`tushare_token_hint='ts-r...90ab'`が返されることを確認した。

**確認方法**: (LLMキーは不要。設定値がプレースホルダーでないことが条件。呼び出し自体はLLMを消費しない)

1. `agent/.env`ファイルを編集し、プレースホルダーを実際のAPIキーに置き換えます。例:`OPENROUTER_API_KEY=sk-or-v1-TestKeyAbcde12345`。サーバーを再起動します。
2. 任意のローカルクライアントから、またはF-A4のクロスオリジンパス経由で、`curl -s "http://127.0.0.1:8899/settings/llm"`を実行します(`Authorization`ヘッダーは不要です)。
3. HTTP 200レスポンスで、`api_key_configured=true`と、実際のAPIキーの最初の4文字と最後の4文字を含む`api_key_hint`が表示されることを確認します。
4. `curl -s "http://127.0.0.1:8899/settings/data-sources"`を実行し、同様のパターンで`tushare_token_hint`が表示されることを確認します。

**影響**: この問題により、設定されたAPIキーの構造化されたバイト情報が漏洩します。特に、Tushareトークンなど、エントロピーが限られたプロバイダーキーに対するオフラインでの推測攻撃と組み合わせることで、この情報の一部が公開されることで、攻撃対象領域が限定されたブルートフォース攻撃の範囲に狭められる可能性があります。この漏洩は、F-A4のクロスオリジンブラウザパスからアクセス可能であり、ローカルTCPアクセスすら不要です。エージェントと同じホストで動作しているブラウザで、オペレーターが悪意のあるローカルWebページを閲覧するだけでアクセスできてしまいます。

---

### 推奨される対策(各問題点について)

1. **F1 / F3** — `require_auth()` 関数は、`API_AUTH_KEY` が設定されていない場合に、必ずエラーを発生させる必要があります。環境変数が空の場合は、起動時にエラーを発生させるか、インストールごとにランダムなキーを生成し、一度限りの初期設定メッセージを表示します。303行にある `if not api_key: return` という簡略化された処理を、開発環境と本番環境を区別した明示的な処理に置き換えます。
2. **F2** — すべての読み込み側の `@app.get()` デコレータ (`/runs*`, `/sessions*`, `/swarm/runs*` など) に `Depends(require_auth)` を適用します。現在のドキュメント文字列(289行目、「この依存関係は書き込みエンドポイントでのみ使用されます」)は、バグを説明しているだけで、設計意図を反映しているわけではありません。
3. **F3** — `_BLOCKED_UPLOAD_EXT` を、禁止リストではなく、許可リスト (`.csv`, `.tsv`, `.json`, `.pdf`, `.txt`, `.xlsx`, `.docx` など) に変更します。ただし、`.py`, `.sh`, `.yaml`, `.j2`, `Dockerfile` は、いずれの場合も拒否対象として扱います。アップロードされたファイルは、エージェントのツール検出範囲外のディレクトリに保存します。
4. **F-A4** — `_CORS_ORIGINS` のデフォルト値を、`http://localhost:5173` (Viteの開発用ポート) のみに変更します。`_is_local_client` を `request.client.host` から切り離し、Bearerトークン *または* `Origin` ヘッダーのチェック(TCPピアIPアドレスではない)のいずれかを必須とします。`_CORS_ORIGINS` にポートを追加すると、認証されたクロスオリジンAPIへの完全なアクセス権が付与されることを、明確にドキュメントに記載します。
5. **F-A5** — `_mask_secret()` 関数を、固定のプレースホルダー ("•••configured•••") または、存在を示すフラグに置き換えます。UIがヒントを必要とする場合は、キーをハッシュ化(SHA-256の短縮形)し、ヒントから元のバイト列を復元できないようにします。

---

影響を受けるパッケージ:
- pip (Python) vibe-trading-ai: 影響範囲は 0.1.0 以上、0.1.7 未満。0.1.7 で修正済み。