署名付きURLで鍵を隠す

更新: 2026-09-26
AIエージェント権限分離MCPHMACNode.js

エージェントに生成物をファイルとして持ち帰らせたい。取得口にBearerトークンを必須にすると、エージェントのシェルにそのトークンが要る。シェルで動くものはプロセスの環境変数を読めるので、親トークンをシェルに置いた時点で、口を絞った意味が薄れる。

ここに置くのは、親トークンを渡さず、その1本のファイルだけを一時的に取れるURLを発行する型だ。経緯は開発日記のAIに動画を作らせる 開けた口は3つだけにある。

型

ツールの返り値に、期限つきの署名付きパスを載せる。エージェントはそれをcurlに渡すだけで、認証ヘッダーを持たない。

/files/<ジョブID>/<番号>.mp4?exp=<期限の秒>&sig=<64桁の16進>署名の入力: file:<ジョブID>:<番号>:<期限の秒>HMAC-SHA256鍵 = 親トークンゲートウェイ側だけ。シェルには無い別のジョブ・別の番号・別の期限へ付け替えると、署名が合わなくなる。
URLの3つの部品と、署名が何に紐づくか。エージェントが見るのはURLだけで、鍵は見えない。

署名の実装は次の1関数で足りる。先頭のfile:は用途の接頭辞で、将来ほかの署名を足しても取り違えないためのもの。

import { createHmac } from 'node:crypto';

export function signDownload(token: string, jobId: string, index: number, expSec: number): string {
  return createHmac('sha256', token)
    .update(`file:${jobId}:${index}:${expSec}`)
    .digest('hex');
}

有効期限は15分。切れたら、ツールをもう一度呼ぶ。呼ぶたびに新しく発行するので、エージェント側に「更新」の処理は要らない。

検証の順番

検証は、署名の比較より前に形式を縛る。順番は次のとおり。

1 形式2 期限3 再計算4 比較5 配るexpは数字のみsigは16進64桁expが現在秒以上か鍵で署名を作り直す定数時間で突き合わせる200で動画を返すどこで落ちても 401(理由は返さない)1を先に置く理由: 長さの違う値を渡すと timingSafeEqual は比較せず例外を投げる。401にしたいところが500になる。
署名の比較の前に、形式と期限を安い順に落とす。

Node.jsのtimingSafeEqualに長さの違う2つを渡すと、比較の前に例外になる。実際に3バイトと4バイトで試した。

RangeError ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH

短い署名を送られたときに500を返してしまわないよう、sigは「16進64桁」だけを通してから比較する。

実測 6通りの悪用

2026-09-26、video-gen/mcpのテスト51件が0.37秒で通った(GPU不要。動画生成は偽の生成器に差し替えてある。この後、Origin検証を足して53件になった)。うち、署名付きURLを悪用する6通りは、実際にHTTPで叩いていずれも401だった。

  • 署名の末尾1文字を書き換える
  • 署名はそのまま、期限だけ1時間延ばす
  • 期限切れ(署名自体は正しい)
  • 同じジョブの別のショット番号へ付け替える
  • 別のジョブのURLへ付け替える
  • 署名を付けない

期限の境目も確かめた。**期限の秒ちょうどはまだ有効で、その1秒後から無効。**署名付きURLで/mcpに入ろうとする試みも401だった。署名で通るのは/filesの該当ファイルだけ、と分けてあるためだ。

残る弱点

  • **URLを持っている間は誰でも、その1本を取れる。**期限は15分。ツールの返り値はエージェントの会話にも入るので、URLごと文脈に残る
  • **署名の鍵は親トークンと同じ値。**親トークンを変えれば、発行済みのURLはすべて切れる
  • **Bearerでも/filesは取れる。**手元の確認用に残してある。エージェントのシェルにBearerが無いことが前提で、これは設定で守る部分
  • MCPの仕様は、Streamable HTTPのサーバーがOriginヘッダーを検証することを求めている(2025-06-18版、DNS再バインディング対策)。このサーバーは最初、Originを見ていなかった(2026-09-26に直した)。Origin付きの要求は認証より前に403にし、許可する送信元だけ環境変数で足す。署名付きURLの/filesには適用しない

関連

WIKI一覧へ