2026-08-09 SIWE の domain / URI / 有効期限をサーバー側で検証する(Issue #29)
- 課題:
docs/development-plan.md4章は「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 のdomainがwindow.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.exampleにlocalhost:3000を既定で記載した。 - 開発サーバーを 3000 以外のポートで動かす場合は、そのポートを含む値へ変更する必要がある。