LoopXのserve-status機能における緩いCORSポリシーにより、コントロールプレーンの状態や、プライベートなMarkdownファイルが公開される可能性があります。

## 概要

`loopx serve-status`は、すべてのレスポンスに対して`Access-Control-Allow-Origin: *`を送信します。
これには、認証を必要としない`GET`リクエストも含まれます。エンドポイントが認証を必要とせず、固定された既知のローカルポートにバインドされているため、**オペレーターがアクセスするどのウェブサイトでも、LoopXのコントロールプレーンの状態や、登録された各ゴールリポジトリ内のMarkdownファイルの内容を、ユーザーの操作なしに、単純な`fetch()`関数を使って読み取ることができます。**

書き込みエンドポイントには影響はありません。これらは正しくオリジンチェックが行われています。このレポートは、**読み込み**に関するものです。

## 影響

`loopx serve-status`が実行されている間、任意のオリジンから以下のJavaScriptコードを実行できます。

```js
// ユーザー操作、プレフライト、認証は不要
const s = await (await fetch('http://127.0.0.1:8766/status.json')).json();
for (const g of s.goals ?? []) {
const m = await (await fetch(
`http://127.0.0.1:8766/review-material?goal_id=${g.id}&path=design-notes.md`
)).json();
exfiltrate(m.content);
}
```

そして、以下の情報を読み取ることができます。

* **`GET /status.json`** — 全体のステータス情報:ゴールID、実行履歴、およびローカルの絶対パス (`registry`, `runtime_root`)。`--global-registry`オプションを使用すると、**マシンに登録されているすべてのプロジェクト**を対象とします(単一のプロジェクトだけでなく)。
* **`GET /review-material?goal_id=…&path=…`** — ゴールのリポジトリのルート、ステートディレクトリ、またはランタイムディレクトリ内の、任意の`.md`または`.markdown`ファイルの**内容**。実際には、オペレーターのプライベートな設計ドキュメント、引き継ぎドキュメント、および証拠に関する記述が含まれます。
* **`GET /`** — エンドポイントの検出。有効になっている書き込みAPIも含まれます。
* **`POST /control-plane/configure-goal/dry-run`** — このルートには、**オリジンチェックが一切行われていません**。また、`do_OPTIONS`は`Access-Control-Allow-Headers: Content-Type`を返すため、クロスオリジンのJSON POSTリクエストはプレフライトを通過します。レスポンスには、ゴールの`before`/`after`のコントロールプレーン設定情報が漏洩します。(ただし、書き込みは行われません。`written=false`であることを確認しました。)

`docs/public-private-boundary.md`では、「ローカル絶対パス」、「タスクID」、「生のログとメトリクス」、および「現在のユーザーコンテキストを明らかにするアクティブな目標状態」を、機密情報として扱うべきものと分類しています。現在、これら全てはどのオリジンからも読み取ることができます。

これを理論上の問題に留めない要因が2つあります。

1. **固定されたデフォルトポート。** `8765`(プロジェクト用)と`8766`(グローバル用)がハードコードされたデフォルト値であるため、攻撃者は両方を試すだけで済みます。探索の必要はありません。
2. **永続的なサービスとしてドキュメント化されている。** `docs/guides/getting-started.md`では、`loopx serve-status`が記述されており、`scripts/macos-dashboard-launchagent.sh`は、`serve-status --global-registry --port 8766`を**ログインごとに実行し続ける**LaunchAgentをインストールします。このガイドに従うユーザーにとっては、常に読み取り可能な、システム全体に及ぶ情報公開状態となります。

私は、この問題を「高」のレベルと評価しました。これは、永続的なサービスから機密のローカル情報を、ユーザーの操作なしに外部に送信される可能性があるためです。読み取り専用であり、範囲が限定されているため、もしあなたがこの問題をより低いレベルと評価するのであれば、私は異議を唱えません。なぜなら、あなたはユーザーの環境設定について、私よりも詳しく知っているはずです。

## 位置

`loopx/status_server.py:136-146` — `StatusRequestHandler._send_json()`:

```python
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # <-- 142行目
```

`_send_json`は、*すべての*ルートに対する唯一のレスポンスパスです。したがって、ワイルドカードは、`/status.json`、`/review-material`、`/healthz`、`/`、およびテスト用のPOSTリクエストなど、すべてに適用されます。
`do_OPTIONS` (`:148-153`)も同様です。

この問題は、執筆時点の`main`ブランチのHEAD (`b0c8e315`, blob `c9e53d1a`)で確認されています。

## 影響を受けないもの(確認済みなので、あなた自身で確認する必要はありません)

報告する前に、この問題に対する反論を試みました。これらの制御はすべて有効です。

* **書き込み操作は適切に保護されている。** `--enable-reward-write-api` と `--enable-control-plane-write-api` の両方を有効にすると、`POST /reward/append` と `POST /control-plane/configure-goal/apply` は、外部の `Origin` に対して **403 "only accepts loopback browser origins"** を返します。 `is_loopback_origin()` が正しく機能しています。 クロスオリジンによる書き込みは不可能であり、DNSリバインディングによっても回避できません(リバウンドしたページは依然として自身のループバックでないオリジンを持っています)。
* **パスのトラバーサルはできない。** `../../../../etc/hostname` → 400; ゴールルートの外部にある絶対パスの `.md` ファイル → 400 ("ゴールリポジトリ/状態/ランタイムのルート外")。 `path_is_under()` が正しく機能しています。
* **拡張子の許可リストが適用されている。** リポジトリ内の `secrets.env` → 400。 読み取り可能なのは `.md` / `.markdown` ファイルのみです。
* **テスト実行では書き込みが行われない。** `configure_goal_with_global_sync(execute=False)` は、書き込み操作を行う前に終了します。 `written=false` となっていることを確認しました。

したがって、アクセス可能な範囲は、登録されたゴールルート下のステータスJSONとMarkdownファイルに限定されます。 これは依然として機密情報ですが、任意のファイルの読み込みやリモートコード実行はできません。

## 実証

HEADの変更されていないクローンに対して、**デフォルトのフラグ**(書き込みAPIは無効)で実行します。

```
GET /healthz Origin: https://evil.example -> 200 ACAO: '*'
GET / Origin: https://evil.example -> 200 ACAO: '*'
GET /status.json Origin: https://evil.example -> 200 ACAO: '*'
GET /review-material?goal_id=victim-goal&path=internal-design-notes.md
Origin: https://evil.example -> 200 ACAO: '*'
content: "# Internal notes\n\nPROD_DB_HOST=db.internal.corp\nCONFIDENTIAL: ..."
```

ファイルの内容が、外部のオリジンに対してそのまま返されました。 同じテスト環境で、**両方の書き込みフラグを有効にした**状態で(つまり、現在の設計にとって最も有利な設定)テストを実行しました。

```
POST /reward/append Origin: https://evil.example -> 403 (オリジンチェック)
POST /control-plane/configure-goal/apply Origin: https://evil.example -> 403 (オリジンチェック)
GET /review-material ../../../../etc/hostname -> 400 (ディレクトリトラバーサルをブロック)
GET /review-material <絶対パスでルート外のファイル>.md -> 400 (ルートチェック)
GET /review-material secrets.env -> 400 (拡張子チェック)
GET /status.json Origin: https://evil.example -> 200 ACAO '*' <-- まだ読み取り可能
```

もし役に立つようでしたら、具体的なテストスクリプトをお送りできます。それは約120行の標準ライブラリのPythonコードで、リポジトリ内のファイルには一切触れていません。

## 提案される修正

このファイルにはすでに正しい条件 `is_loopback_origin()` があります。
最小限の修正は、ワイルドカードの送信を停止し、そのチェックを通過した場合にのみ、リクエストのオリジンをエコーすることです。

```python
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
origin = self.headers.get("Origin")
if origin and is_loopback_origin(origin):
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Vary", "Origin")
# Originヘッダーがない場合 (curl、CLI、同一オリジンからのfetch) -> ACAOを一切送信しない
self.end_headers()
self.wfile.write(body)
```

`do_OPTIONS` でも同様の修正を行います。これにより、ダッシュボードが引き続き機能します。ダッシュボードは `http://127.0.0.1:5174` から提供され、`127.0.0.1:8765/8766` を取得します。これは、2つのループバックポート間のクロスオリジンであり、ワイルドカードが追加された理由はおそらく、すべての非ループバックオリジンを拒否するためだと推測されます。

オプションで、さらにセキュリティを強化するための2つの手順があります。
```

* 既存の `is_loopback_origin()` チェックを、`POST /control-plane/configure-goal/dry-run` および `POST /reward/dry-run` にも適用し、設定情報の読み出しがクロスオリジンで許可されないようにする。
* DNSリバインディング攻撃を防ぐために、`Host` ヘッダーのチェックを追加し(loopback以外の `Host` を拒否)、`Origin` ヘッダーを送信しないクライアントに対する対策とする。

## レビュー範囲

`loopx` パッケージおよびダッシュボードの 758 ファイル (360,618 行) 中の 26/52 のエントリーポイントをレビューしました。使用ツール: semgrep 1.172.0, trufflehog 3.96.0, osv-scanner 2.4.0、および手動によるデータフロー解析。除外対象: `tests/`, `regression/`, `examples/`, `docs/`、および `loopx/benchmark_adapters/` 内のベンチマークアダプター。カバレッジの確度: 部分的。

問題が見つからなかったその他の項目を以下に示します(参考までに)。
自己更新機能(`curl | bash` は、ハードコーディングされた `raw.githubusercontent.com` の URL に対して HTTPS で実行されます。`LOOPX_ARCHIVE_URL` のオーバーライドには、環境制御が必要です。つまり、攻撃者がすでに持つ権限が必要です)。プロジェクト/スラッシュコマンド機能のインストール(厳密な ID 正規表現と `_assert_project_boundary()` による隔離)。`shell=True` のワーカーティック(この値は、設計上、ユーザーが設定するシェルコマンドであり、`demo_supervisor.build_visible_worker_turn_command` が `shlex.quote` を使用してコマンドをエスケープします)。アウトバウンドの SSRF の脆弱性候補 (`content_ops/surface.py` は、プライベート/loopback/リンクローカルの範囲をブロックし、リダイレクトを拒否します)。パッケージ全体で `pickle`, `yaml.load`, `eval`, または `os.system` は使用されていません。

開発/ドキュメントツールに 3 つの依存関係に関する注意点があります (`pymdown-extensions`, `idna`, `postcss`)。これらはランタイムパッケージから到達できません。また、依存関係も宣言されていないため、バージョンアップのプルリクエストは作成していません。

## 発見者

[Aeon](https://github.com/aeonfun/aeon) による、自動セキュリティレビュー。検出された問題は、HEAD ブランチで再現され、報告前に手動で検証されます。報告は、`SECURITY.md` で指定されているインテークチャネルである GitHub PVR を通じて非公開で行われました。

影響を受けるパッケージ:
- pip (Python) loopx: 影響を受けるバージョンは 0.0.1 以上、0.4.5 未満。0.4.5 で修正済み。