READ
~/eforce-tec/blog/posts/form-spam-without-recaptcha.md
markdown
3 min

フォームのスパム対策、reCAPTCHA を入れる前にやること

date2026-09-06author難波 優毅(Masataka Namba)techフォームスパム対策phpセキュリティ3 min

問い合わせフォームにスパムが届き始めると、まず reCAPTCHA を入れる案が出る。手軽で効果もある。ただし代償がある。

  • 「信号機を選んでください」で本物の見込み客が離脱する
  • 判定が厳しいと、視覚に困難のある利用者が送信できなくなる
  • 外部サービスへ、送信者の情報が渡る

問い合わせフォームは1日に数件しか来ない。そこに、全員へ負担をかける仕組みを入れるのは費用対効果が悪い。利用者に何も要求せず、サーバー側だけで完結する方法から順に試すほうがいい。

1. ハニーポット(人には見えない入力欄)

もっとも効果が高く、実装が短い。

人間には見えないが、HTMLを機械的に読むボットには見える入力欄を置く。そこに文字が入っていれば自動送信だと判断する。

<div class="trap" aria-hidden="true">
  <label for="website">この欄は入力しないでください</label>
  <input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
/* display:none ではなく画面外へ飛ばす。
   display:none はボット側も無視することがある */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

サーバー側では、埋まっていたらエラーを返さず、成功したように見せて捨てる。

if (trim((string)($_POST['website'] ?? '')) !== '') {
    header('Location: /thanks/', true, 303);
    exit; // 何も送らない
}

エラーを返すと、ボットの作者に「弾かれた」と伝わり、対策を変えられる。黙って捨てるほうが長持ちする。

NOTE

欄の名前は website や url のように、ボットが埋めたくなる一般的な名前にする。honeypot や spam-trap という名前にすると、ボット側に避けられる。また tabindex="-1" と aria-hidden="true" を付けて、キーボード操作や読み上げソフトの邪魔をしないようにする。

2. 送信元の確認

自サイトのページ以外から投げられた POST を弾く。他所のページにフォームを設置して送りつけてくる手口に効く。

$origin  = $_SERVER['HTTP_ORIGIN'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$ok = ($origin !== '' && strpos($origin, 'https://example.com') === 0)
   || ($origin === '' && $referer !== '' && strpos($referer, 'https://example.com') === 0);

if (!$ok) {
    // 拒否
}

Origin は最近のブラウザなら POST 時に必ず送られる。付いていない場合だけ Referer を見る、という二段構えにしておくと、古い環境の利用者を弾かずに済む。

これは万能ではない。ヘッダは偽装できるので、手間を1段階増やすだけの対策と考えたほうがいい。

3. 同一IPからの連続送信を制限する

同じ相手から短時間に何十件も来る場合に効く。データベースを使わず、ファイル1つで足りる。

function withinRateLimit(string $ip, int $max = 5, int $window = 600): bool
{
    $path = sys_get_temp_dir() . '/form-' . hash('sha256', $ip) . '.txt';
    $now = time();

    $stamps = [];
    if (is_readable($path)) {
        foreach (explode(',', (string)file_get_contents($path)) as $t) {
            $t = (int)$t;
            if ($t > 0 && ($now - $t) < $window) {
                $stamps[] = $t; // 期限内のものだけ残す
            }
        }
    }

    if (count($stamps) >= $max) {
        return false;
    }

    $stamps[] = $now;
    @file_put_contents($path, implode(',', $stamps), LOCK_EX);
    return true;
}

IPはそのまま保存せず、ハッシュ化してファイル名にする。個人情報を余分に持たないためだ。

上限は緩めでいい。10分に5件なら、通常の利用者が引っかかることはまずない。

4. 内容による判定は最後の手段

「特定の単語が入っていたら弾く」という方法もあるが、これは誤判定が怖い。本物の問い合わせに、たまたま該当語が入っていたら失われる。

どうしても入れるなら、弾くのではなく件名に印を付けて受信するほうがいい。判断を人間に残せる。

忘れてはいけないこと

スパム対策と同時に、必ず入れておくべきものが2つある。

メールヘッダ挿入の防止。 名前やメールアドレスの欄に改行を仕込まれると、メールの宛先を書き換えられ、迷惑メールの踏み台にされる。1行入力の項目からは改行を必ず除去する。

$name = str_replace(["\r", "\n", "\0"], '', $_POST['name'] ?? '');

送信失敗時の退避。 スパムを弾くことより、本物を落とさないことのほうが重要だ。メール送信が失敗したときに内容をログへ残しておけば、サーバー側の障害で問い合わせが消えることはなくなる。

それでも足りなくなったら

ここまでやって、なお処理しきれない量が来るようになったら、そのとき初めて画像認証を検討すればいい。

順番が大事で、先に画像認証を入れてしまうと、離脱している見込み客の数は誰にも見えない。まず利用者に負担をかけない方法を尽くしてから、必要なぶんだけ足す。この順番を守るほうが、結果的に問い合わせ件数は多くなる。

$audit --target "your-server"

そのサーバーの中、最後に確かめたのはいつですか。

E Force Tec では、Webサイトとサーバーの診断と監視を「ホワイトハッカーAI社員」として提供しています。置かれたままのバックアップ、駆除後に残った入口、書き換えられたファイル。表示を見ているだけでは気づけない穴を見つけて、塞ぎます。