IT技術ブログ

中小企業の問い合わせフォームのスパム対策を実装する。Turnstile・reCAPTCHAのサーバー側の検証、ハニーポット、送信元の制限、通知メールを届ける設定

中小企業の問い合わせフォームに届くスパムを実装で減らす手順です。Turnstile・reCAPTCHAのトークンのサーバー側の検証、ハニーポット、送信元の回数制限、自動返信を悪用させない作り、通知メールの送信元の設定まで。

この記事の結論:中小企業の問い合わせフォームのスパム対策は、1つの道具に任せず、「ハニーポット」「送信までの時間」「Turnstile や reCAPTCHA のトークンのサーバー側の検証」「送信元の回数制限」「自動返信を踏み台にさせない作り」を重ね、どの層で落としたかを記録する形で実装します。いちばん抜けやすいのは、画面に部品を貼っただけでサーバー側の検証をしていない状態です。もう1つ見落とされやすいのが通知メールで、問い合わせた人のアドレスを送信元(From)にせず、自社のドメインから送って、相手のアドレスは返信先(Reply-To)に入れます。

ここでは、フォームの送信を自社のサーバーやサーバーレスの関数で受け取っている場合の、作る側の手順を扱います。項目の数や入力のしやすさのような見直しの観点は扱いません。根拠にしたのは、Cloudflare と Google の開発者向けの資料、IPA の「安全なウェブサイトの作り方」、Gmail の送信者の要件です(いずれも読んだ日は2026年10月3日)。

問い合わせフォームのスパム対策は、層を重ねて、どの層で落ちたかを残す

フォームのスパムは、送ってくる手口が一様ではないため、性質の違う対策を何層か重ねて減らします。層ごとに、止められる相手と、本物の問い合わせを誤って落とす起きやすさが違います。

層 止めやすい相手 本物を落とす起きやすさ 利用者の手間
ハニーポット(人に見えない欄) 欄をすべて埋める単純なプログラム 低い(自動入力の扱いに注意) なし
送信までの時間 画面を開いた直後に送るプログラム 低い(下限を短めにすれば) なし
Turnstile・reCAPTCHA の検証 ブラウザを使わずに直接送るもの、機械的な操作 中くらい(設定しだい) ほぼなし〜確認の操作
送信元の回数制限 同じ送信元からの大量の投稿 低い(社内から試すときは注意) なし
入力内容の検査 URLだらけの本文、決まった文面 中くらい なし
メールの作り 自動返信を使った第三者への送りつけ なし なし

どの層で落ちたかを記録しておくのは、あとで「本物を落としていないか」を確かめるためです。落とした投稿は捨てず、理由(どの層か)と時刻を付けて、決めた期間だけ保管しておきます。

Turnstile・reCAPTCHA は、サーバー側でトークンを検証して初めて効く

Turnstile も reCAPTCHA も、画面に置いた部品はトークンを発行するだけで、その真偽を確かめるのはフォームを受け取るサーバーの役目です。サーバーで検証用のAPIを呼ばずに処理を進めると、ブラウザを通さずに直接送られた投稿は、そのまま通ります。

項目 Turnstile(Cloudflare) reCAPTCHA v3(Google)
検証の送り先 https://challenges.cloudflare.com/turnstile/v0/siteverify https://www.google.com/recaptcha/api/siteverify
送るもの 秘密鍵(secret)とトークン(response) 秘密鍵とトークン
トークンの有効期間 発行から300秒 2分
同じトークンの再利用 1回しか検証できない(2回目は timeout-or-duplicate) 期限内に1回の検証を前提にする
判定の返り方 成功か失敗か(success) 0.0〜1.0の点数(score)。既定の目安は0.5
あわせて見る値 hostname、action hostname、action

reCAPTCHA は、Google のドキュメントで Google Cloud の不正対策の製品群(Google Cloud Fraud Defense)の一部として案内されるようになっており、v3 の旧来のページには非推奨の表示が出ています。新しく入れる場合は、Google Cloud 側の案内に沿って鍵を作るかどうかから確かめます。

Turnstile を Cloudflare の Workers、または Pages の関数で検証する例です。成功だけでなく、ホスト名と action が自分のフォームのものかも確かめます。

js
// フォームの受け口(Cloudflare Pages Functions の例)
export async function onRequestPost({ request, env }) {
  const form = await request.formData();
  const token = form.get('cf-turnstile-response') || '';

  const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: new URLSearchParams({
      secret: env.TURNSTILE_SECRET,              // 秘密鍵は環境変数に置き、コードに書かない
      response: token,
      remoteip: request.headers.get('CF-Connecting-IP') || '',
    }),
  });
  const v = await res.json();

  const ok = v.success && v.hostname === 'www.example.co.jp' && v.action === 'contact';
  if (!ok) {
    await logRejected('turnstile', v['error-codes']); // どの層で落ちたかを残す
    return new Response('送信を受け付けられませんでした。時間をおいて再度お試しください。', { status: 400 });
  }
  // ここから先で、ハニーポット・回数制限・保存・メール送信へ進む
}

Turnstile の部品には、利用者の様子に応じて確認の操作を出すかを自動で選ぶ Managed、操作を求めずに読み込み中の表示だけを出す Non-Interactive、何も表示しない Invisible の3つの型があります。問い合わせフォームでは、まず Managed で始め、確認の操作が出る割合を見て変えるかを決めます。

トークンの期限が短いため、入力に時間がかかった人が送信したときに期限切れで失敗することがあります。失敗したときは入力内容を消さずに画面に残し、部品を読み込み直して再送できるようにしておきます。

ハニーポットは、人の目にも読み上げにも出さず、自動入力に埋められない欄にする

ハニーポットは、人には見えない入力欄を1つ置き、そこに値が入っていたら機械の送信とみなす仕組みです。作るときは、画面に出さないだけでなく、キーボードの移動や読み上げのソフトでも触れられないようにし、ブラウザの自動入力で埋まらない名前を付けます。

html
<div class="hp" aria-hidden="true">
  <label for="hp_company_url">この欄は空のままにしてください</label>
  <input id="hp_company_url" name="hp_company_url" type="text"
         tabindex="-1" autocomplete="off" value="">
</div>
css
/* display:none だけだと読み飛ばすプログラムがあるため、画面の外へ出す */
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

決めておくことは3つです。

  • 欄の名前に email や name のような、自動入力が反応しやすい語を使わない
  • 値が入っていたときは、画面には通常の完了を返し、保存とメールの送信だけをしない(落としたことを相手に知らせない)
  • あわせて、画面を表示した時刻を隠し値で持たせ、表示から数秒以内に送られたものも同じ扱いにする。下限は、自動入力で速く送る人を落とさない短さにする

送信元の制限は、回数の上限とOriginの確認から始める

同じ送信元から短い間に何度も送られるスパムは、送信元ごとの回数の上限で止めます。あわせて、フォームの受け口がPOSTだけを受け、自分のサイトのページから送られたものかをOriginヘッダーで確かめます。

確かめること 実装の例 注意
送信の方法 POST以外は 405 を返す 受け口のURLを直接開いても何も起きないようにする
送り元のページ Origin が自分のドメインでなければ断る 自分のドメインの www あり・なしの両方を許す
回数の上限 IPアドレスごとに、決めた時間の中の送信の回数を数える 社内やテストで続けて送ると引っかかる。上限は低くしすぎない
本文の大きさ 本文の文字数と、項目ごとの長さに上限を付ける 相談内容の欄は、普通の相談が収まる長さにする
本文の中身 URLの数が多い本文を「要確認」にする 落とさずに保留にし、人が見て判断する

Cloudflare を使っている場合、レート制限のルール(Rate limiting rules)で、特定のパスへのリクエストの回数に上限を設けられます。使えるルールの数や数える時間の長さは契約のプランで違うため、受け口の関数の中でも回数を数える形にしておくと、プランに左右されません。

自動返信のメールを、他人宛ての迷惑メールの踏み台にしない

問い合わせフォームの自動返信は、入力されたアドレスにメールを送る仕組みなので、他人のアドレスを入れて送られると、自社のドメインから知らない人へメールを送ることになります。自動返信には相手が書いた本文を載せず、検証をすべて通った投稿にだけ送るようにします。

  1. 自動返信は、Turnstile などの検証、ハニーポット、回数制限を通ったあとにだけ送る
  2. 自動返信の本文に、相談内容や名前の欄に書かれた文をそのまま載せない(受け付けた日時と、返信の目安だけにする)
  3. 同じ宛先への自動返信は、短い間に何通も送らないよう上限を付ける
  4. 件名や宛先のようなメールのヘッダーに、入力された値を入れない。入れる場合は改行を取り除く
  5. 宛先をフォームの隠し項目に書かない。宛先はサーバーの側の設定だけで決める

4と5は、IPA の「安全なウェブサイトの作り方」がメールヘッダ・インジェクションの対策として挙げている内容です。ヘッダーを固定の値にし、外から入った値は本文にだけ出すこと、宛先をHTMLで指定しないことが、根本的な解決として示されています。

通知メールは自社のドメインから送り、問い合わせた人のアドレスは返信先に入れる

社内への通知メールの送信元(From)に、問い合わせた人のアドレスを入れると、相手のドメインの認証に通らず、迷惑メールに入ったり届かなかったりします。送信元は自社のドメインのアドレスにし、問い合わせた人のアドレスは返信先(Reply-To)に入れます。

ヘッダー 入れる値 理由
From 自社のドメインの送信用アドレス(例:[email protected]) 自社のドメインで SPF・DKIM を設定でき、認証が通る
Reply-To 問い合わせた人のアドレス 通知メールに「返信」すると、そのまま相手に届く
To 社内の複数人が見る共有のアドレス 担当者の不在や退職で、通知が見られなくなるのを防ぐ
件名 固定の文字列+受付の番号 入力された値をヘッダーに入れない

Gmail の送信者の要件では、送る件数にかかわらず、SPF か DKIM のどちらかの認証を送信元のドメインに設定することと、Gmail の From ヘッダーをなりすまさないことが挙げられています。1日に5,000件を超えて送る送信者には、さらに SPF と DKIM の両方、DMARC の設定、From のドメインを SPF か DKIM のドメインと一致させることが求められます。問い合わせの通知は件数が少なくても、自社のドメインの側で SPF と DKIM、DMARC の3つを設定しておけば、届かない原因を1つ減らせます。

メールが届かなかった場合に備え、投稿はメールを送る前にデータベースなどへ保存しておきます。送信に失敗したら、保存した投稿から送り直せるようにしておけば、問い合わせの中身は手元に残ります。確認メールの送信元ドメインの設定は、フェア予約の送信を確かめる記事でも、送信の前に確かめる項目として書いています。

対策を入れたら、落とした件数と本物の送信を両方確かめる

スパム対策は、入れた日より、そのあとの見直しで差が出ます。層ごとに落とした件数を週ごとに見て、急に増えた層、まったく働いていない層がないかを確かめ、保留にした投稿の中身も人が目を通します。

  • 層ごとの件数:ハニーポット、時間、Turnstile、回数制限、本文の検査のそれぞれで、何件落としたか
  • 保留の中身:URLの多さで保留にした投稿に、本物の相談が混ざっていないか
  • 実際の送信:対策の設定やサイトを変えたら、スマホとパソコンから1件ずつ送り、社内への通知と自動返信の両方が届くか

株式会社bundlyzeでは、静的なホスティングにサーバーレスの関数を組み合わせた自社サイトでLLMO(生成AIの答えに会社の情報を取り違えなく載せる取り組み)を実践し、やったことの記録も公開しています。フォームの受け口の実装やメールの送信の設定を含めたサイトづくりは、ホームページ・LP制作のページで紹介しています。

参照した公式の資料(読んだ日:2026年10月3日)

よくある質問

TurnstileやreCAPTCHAをフォームに貼れば、スパムは止まりますか?

画面に部品を貼るだけでは止まりません。部品が発行するトークンを、フォームを受け取るサーバーの側で提供元の検証用のAPIに送り、成功の応答を確かめてから処理を進める必要があります。検証を省くと、トークンを付けずにサーバーへ直接送られた投稿が、そのまま通ってしまいます。

ハニーポットだけで対策しても大丈夫ですか?

入力欄を見境なく埋める単純なプログラムには効きますが、フォームの作りを読んで隠れた欄を避けるものには効きません。ハニーポットは誤判定が少なく、利用者の手間もない最初の層として使い、TurnstileやreCAPTCHAの検証、送信元の回数制限と重ねて使います。

スパム対策で、本物の問い合わせまで落としていないか心配です。

どの層で、何件落としたかを記録しておくと確かめられます。落とした投稿は捨てずに、理由と一緒に一定の期間だけ保管し、ときどき中身を見て本物が混ざっていないかを確認します。あわせて、スマホから実際に送って、通知と自動返信の両方が届くことを、対策を変えるたびに確かめます。