コンテンツにスキップ

配信とセキュリティの仕組み

Miseru の配信は「GitHub PR に画像を表示できること」と「見せたい間だけ見せること」の両立を目的に設計されています。このページでは、その仕組みと保証の境界線を解説します。

GitHub は PR コメント内の外部画像を、そのまま読み込まず Camo という画像プロキシ経由で表示します。Camo には次の性質があります。

  • 閲覧者の認証情報は外部サーバーに渡らない → 配信側で「誰が見ているか」による認可はできない
  • 取得した画像をキャッシュする → オリジンを消してもキャッシュが残りうる
  • 5MB 以上の画像は表示しない

Miseru の設計制約(Cache-Control の明示・失効順序・5MB 制限)はすべてこの実測結果から導かれています。

配信 URL は次の形式です。

https://img.miseru.dev/i/<id>.png?exp=<unixtime>&token=<kid>.<hmac>
要素内容
id128bit のランダム ID(32桁hex)。総当たりや推測は現実的に不可能
exp署名の有効期限(Unix 時刻)。アップロードから365日
token<id>:<exp> に対する HMAC-SHA256 署名。kid は鍵バージョン

配信 Worker は次を順に検証し、ひとつでも失敗すればコンテンツを返しません。

  1. exptoken が揃っているか(欠落は 403)
  2. exp が未来か(過去なら 403)
  3. HMAC 署名が正しいか(改竄・偽造は 403。比較は定数時間で行いタイミング攻撃を防止)
  4. 画像が存在し、失効していないか(失効済み・削除済みは 404)

kid(key version)により、署名鍵をローテーションしても発行済み URL は壊れません。

exp は「失効ポリシーが許す保持期間全体をカバーする上限」です。Camo のキャッシュは POP(拠点)ごとに独立しており、キャッシュによる延命は保証されません。exp を短くしてもセキュリティは向上せず(公開停止は失効=削除で行うため)、再取得時に画像が壊れるだけなので、長めに設定しています。実際の公開停止は常に失効ポリシーによる削除が優先されます。

配信レスポンスには必ず Cache-Control を明示します(無指定だと Camo が約1年のキャッシュを付与するため)。

Cache-Control: public, max-age=<残り保持秒数>, immutable

max-age は失効ポリシーに連動して導出されます。

ポリシーmax-age
pr_close最大300秒(近時失効の上限。PR クローズ後、キャッシュ込みで最大5分程度で表示が止まる)
retain_days保持期限までの残り秒数(期限が近づくほど短くなる)
forever署名の期限まで(上限1年)

失効は必ず次の順序で実行されます。

  1. R2(オリジン)から削除 — 強整合。この時点で再取得は必ず 404
  2. メタデータ(D1)から削除

オリジン削除を必ず先に確定させるのは、オリジンが生きているうちにキャッシュ層の再取得が起こると 200 が再キャッシュされて失効が延命されてしまうためです。

すでにキャッシュされた配信(GitHub Camo)への能動的なパージは行わず、Cache-Control の期限による自然失効に委ねます。キャッシュが切れるまでの時間は上記のポリシー連動 max-age で決まります: pr_close はクローズから最大5分程度retain_days は保持期限まで、forever の手動失効ではキャッシュ済みの拠点で保持期間が切れるまで表示が残ることがあります。

途中で失敗した場合、その画像は失効予約としてマークされ、毎時の自動スイープがリトライして完了させます。

レイヤー失効後の挙動保証
オリジン(R2)即座に削除✅ 確実
配信 URL(img.miseru.dev)404✅ 確実
Camo キャッシュCache-Control による自然失効(pr_close は最大5分程度)⚠️ 即時消去は保証されない
閲覧者の手元スクリーンショット・保存済み画像❌ 制御不能

守られること: URL を知らない第三者が画像に到達することは、推測不能 ID + HMAC 署名により現実的に不可能です。失効後は新規取得が必ず失敗します。

守られないこと: URL を知っている人は、失効までの間は誰でも閲覧できます(閲覧時の認可なし)。また、一度表示された画像は Camo キャッシュや閲覧者の保存によって失効後も一定期間・恒久的に残る可能性があります。

この境界線が受け入れられない情報(認証情報・個人情報・未公開の機密画面など)は、そもそもアップロードしないでください

  • Upload Token — サーバーには SHA-256 ハッシュのみ保存。平文は発行時のレスポンスにしか現れません
  • ログインセッション — HttpOnly / Secure / SameSite=Lax の Cookie で7日間。GitHub OAuth のコールバックは state を二重検証(Cookie + サーバー側保存)して CSRF を防ぎます
  • 画像へのアクセス制御 — API 経由の画像操作(失効など)は、トークンが属するプロジェクトの画像に限定されます。他プロジェクトの画像 ID を指定しても、存在の有無すら返しません(一律 404)