MimiCatLab
← ホームに戻る
Unity

localhost だから安全ではない:HttpListener の定数時間比較とサイズ上限

Stream Deck から Unity アプリを操作する localhost HTTP。トークン比較のタイミング攻撃、chunked 転送で素通りする Content-Length 上限、裏スレッドからの Resources.Load。

何に困っていたか

Stream Deck や外部スクリプト、監視ツールから、24 時間配信で動く MimiCatHub 製アプリを同じ PC 内の HTTP で操作・監視したい。「12 時間で別枠へ移す」「配信停止を検知して再起動する」といった自動化を Unity の外から駆動する入口が必要だった。

Remote パッケージは GET /healthPOST /commands/<name> の 2 つだけを持つ。

curl -H "Authorization: Bearer <token>" http://127.0.0.1:17800/health
{"ok":true,"state":"Initialized","uptimeSeconds":123.4,
 "modules":[{"id":"mimicathub.save","state":"Initialized","health":{"state":"Healthy"}}]}

設計

  • バインド先は 127.0.0.1 固定で設定変更不可。外部からの操作は SSH ポートフォワードなど PC 側の手段に委ねる。HTTPS は持たない。
  • 認可はトークン 1 つ。.envMIMICATHUB_REMOTE_TOKENAuthorization: BearerX-MimiCatHub-Token で受け取る。値が無ければ初期化に失敗する。Develop でも必須にして「開発中だけ認可なし」を作らない。
  • HttpListener の受付・本文読み取り・応答書き込みは裏スレッド、コマンド実行は必ずメインスレッド(Tick)。ConcurrentQueue に積み、RunContinuationsAsynchronously の完了源で待つ。コマンドは Task.WhenAny でタイムアウトし 504 を返す。
  • 振り分け(トークン照合 401 → パス解決 404/405 → コマンド検索 404 → 実行)は純粋関数にして、サーバーを差し替えてテストする。
  • JSON は自前の最小実装で、外部ライブラリに依存しない。

何が起きたか

トークン比較が定数時間でなかった。 文字列比較は先頭から不一致を見つけた時点で返るので、応答時間からトークンを推測できる。localhost 限定とはいえ、同じ PC 上の別プロセスが攻撃者になりうる。長さや一致位置で処理時間が変わらない比較へ変更した。

Content-Length の無い要求で上限が効かなかった。 本文の上限(既定 65536 バイト)は申告値で判定していたので、chunked 転送では本文全体をメモリへ読み切ってから判定していた。上限を超えた時点で読み取りを打ち切り 413 を返すようにした。

裏スレッドから Resources.Load を呼んでいた。 警告ログの色を裏スレッドで初めて評価すると、内部で Resources.Load が走ってメインスレッド外の例外になった。色は生成時(メインスレッド)で取得するようにした。

教訓

  1. 秘密情報の比較は、経路が localhost でも定数時間にする。 攻撃者は「ネットの向こう」だけではない。
  2. サイズ上限は申告値ではなく実読み取り量で強制する。 chunked 転送は申告値のチェックを素通りする。
  3. Unity では「どのスレッドで最初に評価されるか」が事故になる。 裏スレッドで触る値はメインスレッドで先に確定させる。
  4. HTTP の受付とゲームロジックをキューと Tick で分けると、Unity API の安全性とタイムアウト制御が両立する。
  5. 「開発中だけ認可なし」を許さないと、後から締める工数と事故確率の両方が消える。

対象外

LAN やインターネットからの接続、HTTPS、サーバーからの通知(Webhook は Discord パッケージの責務)、コマンド別トークンは非対応にした。取得はポーリングだけである。

Related Articles