Skip to content

2026-08-09 SIWE の domain / URI / 有効期限をサーバー側で検証する(Issue #29)

  • 課題: docs/development-plan.md 4章は「SIWE メッセージはサーバーが発行する一回限りの nonce と有効期限を持ち、domain、URI、chain ID、署名対象アドレスをサーバーで検証する」と定めていたが、実装で確実に検証できていたのは chain ID と署名対象アドレスと nonce だけだった。
    • domain の比較対象がリクエストの Host ヘッダーだった。署名メッセージと Host はどちらも送信側が決められるため、「このサービス宛の署名か」の確認になっていなかった。
    • uri を参照している箇所がなかった。
    • 有効期限は expirationTimeあれば siwe ライブラリが検証する形で、メッセージを組み立てるのはクライアントであるため、期限のないメッセージが通る状態だった。

決定

1. 期待するドメインはカンマ区切りの許可リストを環境変数で持つ

SIWE_ALLOWED_DOMAINS を追加し、Host ヘッダーの参照をやめる。

SIWE_ALLOWED_DOMAINS=localhost:3000
  • 書式は ADMIN_ADDRESSES に合わせてカンマ区切り・前後の空白を無視・小文字へ正規化する。ポートを含む(SIWE の domainwindow.location.host 由来のため)。
  • 未設定なら検証は必ず失敗する(domain not configured)。設定漏れが「誰でも通る」側に倒れないようにする。
  • 単一値の環境変数ではなく許可リストにしたのは、Vercel のプレビュー環境や将来の独自ドメイン移行で複数のホスト名を同時に受け付ける必要が出るため。プレビュー環境のドメインは運用側でリストへ追記する。
  • VERCEL_URL を自動で許可する分岐は今回入れない。本番デプロイが延期中(2026-08-06-phase-1-local-validation.md)で挙動を確認できないため、必要になった時点で改めて決める。

2. uri は許可済み domain と同じホストを指すことを確認する

siwe ライブラリは uri を期待値と突き合わせないため、lib/siwe.ts で次を確認する。

  • URL として解釈できること
  • スキームが http: または https: であること
  • ホストが検証済みの domain と一致すること

domain を先に許可リストで確認しているので、uri のホストも同じ許可リストの範囲に収まる。

3. 有効期限はサーバー側で必須にし、上限も設ける

  • expirationTime がないメッセージは受け付けない(expiration required)。
  • 期限切れのメッセージは受け付けない(expired)。
  • 有効期限までの残り時間の上限を MAX_SIWE_LIFETIME_MS = 15分とする(expiration too long)。クライアント(components/SignInWithEthereum.tsx)が入れる期限は10分で、差の5分はサーバーとクライアントの時計のずれを吸収するための余裕。上限がないと、クライアントを差し替えて長期間有効な署名を作れてしまう。
  • 現在時刻は now パラメータで注入できるようにし、siwe ライブラリの verify にも同じ時刻を time として渡す。テストを実時刻に依存させないため。

含めなかったもの

Issue #29 が明示的にスコープ外としたもの。

  • セッション Cookie の属性
  • チェーン変更・アカウント変更時のセッション無効化

影響

  • 既存の .env.local を使っている場合、SIWE_ALLOWED_DOMAINS を追加しないとサインインできなくなる。.env.examplelocalhost:3000 を既定で記載した。
  • 開発サーバーを 3000 以外のポートで動かす場合は、そのポートを含む値へ変更する必要がある。

秘密情報(SESSION_PASSWORD、Supabaseキー、Safeの認証情報)はドキュメント・Issue・ログへ貼らないでください。