- 著者
- app-hacks
- 公開日
- 更新日
- 確認日
Cloudflare Turnstile / Cloudflare Pages / Security
お問い合わせフォームでCloudflare Turnstileをサーバー側検証する
明示的レンダリングとPages FunctionのSiteverifyで、問い合わせ送信を二段階で確認する実装記録。
お問い合わせページはTurnstileを明示的にレンダリングし、認証トークンを取得するまで送信ボタンを無効にしている。ウィジェットの表示だけを送信許可の根拠にはしていない。ブラウザは改変できるため、同じトークンをFunctionへ渡し、サーバー側でも検証が成功した場合にだけメール送信へ進む。
ブラウザ側の状態を限定する
src/pages/contact.astroではrender=explicitでスクリプトを読み、action: 'contact'を指定してturnstile.render()を呼ぶ。callbackでトークンを保持して送信可能にし、期限切れ・エラー・送信完了後にはreset()してボタンを再び無効にする。スクリプトが読み込めないまま待ち続けないよう、10秒後に読み込み失敗として扱う。
widgetId = api.render(widgetContainer, {
sitekey: turnstileSiteKey,
action: 'contact',
callback: (token) => { turnstileToken = token; },
'expired-callback': resetTurnstile,
});
Site keyは画面で使うため公開変数として渡す。一方、Secret keyはFunctionだけで使うSecretであり、HTMLやブラウザJavaScriptへ出さない。公開範囲が異なるため、同じ設定ファイルやログにまとめて書かない。
Functionで再確認する
functions/api/contact.tsはSiteverifyへフォームエンコードでPOSTし、10秒のタイムアウトを付ける。成功フラグに加え、リクエスト先と一致するhostnameを確認する。本番ホストではactionがcontactであることも確認するため、別画面用のトークンを問い合わせ送信へ流用しない。
if (!response.ok || result.success !== true || result.hostname !== expectedHostname) {
return false;
}
return expectedHostname !== 'app-hacks.net' || result.action === 'contact';
期限切れや読み込み失敗ではメールを送らず、利用者には認証のやり直しを案内する。Turnstileは入力値検証やhoneypotの代替ではないため、それぞれをFunction側に残している。
失敗しやすい点と確認方法
クライアントcallbackだけで送信を許可しないこと、タイムアウト時に例外を送信成功として扱わないことが重要である。公開後は正常送信、期限切れ、認証読み込み失敗を分けて確認し、Functionが検証失敗時にメール送信しないことを確認する。
トークンの取得後にフォーム内容を編集する操作はあり得るため、認証済みという画面状態を長く保持する設計にも注意する。実装では送信後に必ずトークンをリセットし、失敗した送信を同じトークンで繰り返さない。利用者へは再認証が必要な理由を短く表示し、認証基盤の内部エラーを詳細に露出しない。
この確認は、実装を増やすためではなく、認証の結果を送信許可へ正しく結び付けるためのものである。
公式資料と関連記事
- Cloudflare公式: Validate the token — Siteverifyとhostname・action検証の確認先。
- Cloudflare Pages FunctionsからEmail Service REST APIで問い合わせを送信する — 認証後の送信処理を扱う。
- お問い合わせ — 現在のフォーム実装。