本文へ移動
aapp-hacks
← 記事一覧へ
著者
app-hacks
公開日
更新日
確認日

Cloudflare Pages / Cloudflare Email Service / Security

Cloudflare Pages FunctionsからEmail Service REST APIで問い合わせを送信する

問い合わせFunctionの入力検証、Turnstile検証、Email Service REST API呼び出しを分離した実装記録。

問い合わせフォームの送信処理はfunctions/api/contact.tsに置いている。ブラウザから送信サービスを直接呼ぶと、認証情報をクライアントへ出すことになるため、POST専用のPages Functionを境界にした。Functionは入力を検査し、Turnstileの検証が成功したときだけCloudflare Email Service REST APIへ送信する。

お問い合わせフォームからの送信を、Pages Functionの入力検証とTurnstile検証を経てEmail Service APIへ渡す構成図

ブラウザは送信APIの認証情報を持たず、Pages Functionを境界として入力・認証・送信を順に扱う。

Functionで最初に拒否するもの

onRequestPostでは、リクエストURLと同じOriginか、Content-TypeがJSONかを確認する。本文はContent-Lengthだけでなくストリームを読みながら上限を検査するため、ヘッダーがない場合も過大な本文を受け入れない。JSONでない、配列である、必須値が不足する、といった要求はメール送信前に失敗レスポンスへ変換する。

if (request.headers.get('Origin') !== expectedOrigin) return fail(403);
if (contentType !== 'application/json') return fail(415);
const payload = await readJsonBody(request);
if (!payload) return fail(400);

名前・件名・本文・トークンには長さの上限を置き、メール形式と本文の最小長も確認する。画面のrequiredだけでは直接APIを呼ぶ要求を防げないため、サーバー側で同じ境界を持つ必要がある。隠し入力のhoneypotが埋まった場合は成功形式の応答を返して処理を終え、送信の有無を攻撃側へ伝えない。

送信処理の責務

必要な環境変数が不足した場合は503、Turnstileの通信・検証に失敗した場合は403または502を返す。送信APIが成功を示さない場合も502とし、送信元・受信先・APIトークンの値をレスポンスへ含めない。送信メッセージの返信先にはフォーム入力のメールを設定するため、運営側は返信経路を分けて扱える。

この構成はCloudflare Email Service REST APIを使う実装であり、別のメール送信基盤を前提にしない。設定する変数名と公開範囲はdocs/contact-form-setup.mdに記録し、実値はGit、ソース、ブラウザ、ログへ出さない。

失敗しやすい点と確認方法

送信成功を画面表示だけで判断せず、公開環境のフォームから送信し、受信とReply-Toを確認する。失敗時はHTTPステータスで分類しつつ、利用者には秘密情報や内部エラーを返さない。POST以外のメソッドを実装していない点も、意図しない利用面を増やさない判断である。

Functionが返すJSONはokだけに抑えており、どの検証で失敗したかを外部へ詳しく返さない。問い合わせフォームでは、入力補助はブラウザで行い、境界の防御はサーバーで行う。役割を分けることで、画面の改善と送信権限の管理を別々に変更できる。

メール本文は入力された値を組み立てて送信するが、外部APIの応答本文は利用者へ転送しない。送信サービス側の一時的な失敗と、フォームの入力不備を同じ表示にしないため、Functionの応答と画面側の案内を役割ごとに保つ。

公式資料と関連記事