ポートフォリオサイトのお問い合わせフォームは、ヘッドレスフォームサービス「HyperForm」のAPIにPHP経由で送信していました。あるときテスト送信してみると、HyperFormの管理画面には送信内容が届いているのに、自分宛ての通知メールもお客様宛ての自動返信も届いていませんでした。
原因は、reCAPTCHAトークンの検証が毎回失敗し、すべての送信がスパム扱いになっていたことでした。同じ構成の方の参考になればと思い、調べた手順と対処を残します。
構成
静的サイト(Next.jsの静的エクスポート)なので、フォームの送信先はサーバー上のPHPです。
- ブラウザで reCAPTCHA v3 のトークンを取得し、入力内容と一緒にPHPへ送信
- PHPで reCAPTCHA のトークンを検証
- 検証に通ったら、同じトークンを付けて HyperForm の API(
/api/<フォームID>/complete)に送信
症状
- 画面には「送信しました」と表示される
- HyperFormの管理画面にも送信内容は保存されている
- ただし配信状況は「メール送信に失敗(スパムの可能性が高い)」で、スパムスコアは 0
スパムスコア0の意味
HyperFormのマニュアル(スパムスコアとスパム判定閾値)によると、スパムスコアは reCAPTCHA の判定結果を記録したもので、次の値は固定で付くそうです。
0:reCAPTCHAトークンの検証に失敗した0.8888:API(非同期API)での送信(reCAPTCHAは動かない)
スコアが閾値(既定は0.65)を下回ると、送信内容は保存されますが通知メールと自動返信は送られません。つまり「閾値を下げれば届く」という話ではなく、トークンの検証そのものが失敗していたことになります。
原因
思い当たる原因が2つありました。
1. 同じトークンを二重に検証していた
reCAPTCHAのトークンは1回しか検証できません。PHPで先に検証してから同じトークンをHyperFormに渡していたため、HyperForm側の検証は「使用済み」として失敗します。
2. HyperForm側は別のキーで検証している(と思われる)
PHPでの検証をやめてトークンをそのまま渡すようにしても、スコアは0のままでした。HyperFormはHyperForm側のキーでトークンを検証していると考えられ、サイト独自のサイトキーで作ったトークンでは検証に通らないようです(ここは推測です)。
対処:非同期APIに切り替える
HyperFormのマニュアルには、サーバーやアプリから送る方法として非同期APIが載っています。
- エンドポイントは
https://hyperform.jp/api/async/<フォームID>/complete(JSONでPOST) - reCAPTCHAは不要で、スパムスコアは常に0.8888
- 事前にセキュリティ設定で「送信元ドメインを制限する」を有効にし、自分のドメインを許可しておく
- サーバーから送る場合は
Originヘッダーに許可したドメインを付ける - 1つのIPから1分に5回まで
非同期APIではHyperForm側のreCAPTCHAが動かないので、reCAPTCHAの検証はPHPで行います。今度はHyperFormがトークンを検証しないので、二重検証の問題も起きません。
// reCAPTCHA の検証(action / hostname / score も確認する)
$recaptcha = json_decode($recaptchaBody, true);
if (
($recaptcha['success'] ?? false) !== true ||
($recaptcha['action'] ?? '') !== 'contactForm' ||
($recaptcha['hostname'] ?? '') !== 'example.com' ||
($recaptcha['score'] ?? 0) < 0.5
) {
// 失敗
}
// 非同期APIに送信(Origin ヘッダーを付ける)
$ch = curl_init('https://hyperform.jp/api/async/xxxxxxxx/complete');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['name' => $name, 'email' => $email, 'message' => $message]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Origin: https://example.com'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
切り替えたところ、スパムスコアは0.8888になり、通知メールも自動返信も届くようになりました。閾値は0.65のままにしています(0にするとハニーポットなど他の判定が効かなくなるため)。
注意点
- 非同期APIのURLを直接叩かれると、PHPでのreCAPTCHA検証は迂回されます。HyperFormのハニーポット(
__hpc__を含む項目名の隠しフィールド)なども併用すると安心です。 - PHPで同じ旧APIの作りを引き継いでいたため、おそらく以前からメールが届いていなかったと思われます。フォームは作って終わりにせず、定期的にテスト送信して確認するのが大事だと感じました。
その後
最終的には、HyperFormをやめてXserverのsendmail(PHPの mb_send_mail)で通知と自動返信を送る形に変えました。送信履歴の管理画面は無くなりますが、外部サービスのスパム判定や送信回数の上限を気にしなくてよくなりました。