Contact Form 7 のスパム対策:Turnstile と「日本語チェック」を入れる。Code Snippets の保存が 403 になったときの回避も

Contact Form 7 のスパム対策:Turnstile と「日本語チェック」を入れる。Code Snippets の保存が 403 になったときの回避も

このブログのお問い合わせフォーム(Contact Form 7)に迷惑メールが届くので、スパム対策を足しました。かかったお金は0円で、画面の操作はスクリーンショットの時刻で見ると20分ほどでした。

届いていたのは、こんなメールです。フォームから送信があると届く通知メールで、英語のものも、日本語のものもありました。

お問い合わせフォームから届いた迷惑メール2通。左は英語で、題名「Your mailing list」、本文は「Hello! Please send me updates about weekly updates.」から始まる。右は日本語で、題名「メール最新情報」、本文は「詳しい情報を希望します。メールでご連絡ください — お問い合わせ.」。差出人と宛先はモザイク
お問い合わせフォームから届いた迷惑メール2通。左は英語で、題名「Your mailing list」、本文は「Hello! Please send me updates about weekly updates.」から始まる。右は日本語で、題名「メール最新情報」、本文は「詳しい情報を希望します。メールでご連絡ください — お問い合わせ.」。差出人と宛先はモザイク

どちらも、中身のない短い文で返信を求める形です。日本語のほうも、文の最後に「お問い合わせ.」と、ページのタイトルらしき言葉がそのまま付いていて、自動で送られたものだと思います。

ほかに、フォームを通らずにメールアドレスへ直接届く迷惑メールもありました。こちらはフォームの対策では止まらないので、メールソフトの迷惑メール判定で振り分けています。この記事では、フォームから届くものの対策を書きます。

途中で、コードを保存しようとしたら 403 で保存できない、ということが起きました。その原因(サーバーの WAF)と回避のしかたも書いておきます。

目次

解決方法

入れた対策は次の3つです。

対策 何を止めるか 使ったもの
Turnstile ボット(自動送信)。日本語の文を送ってくるものも含む Contact Form 7 の標準機能 + Cloudflare(無料)
ハニーポット 見えない入力欄に書き込むボット WP Armour(もともと入っていた)
日本語チェック 英語だけのスパム Code Snippets に PHP を数行

上の日本語の迷惑メールのように、日本語で送ってくるボットもいます。日本語チェックだけでは止まらないので、Turnstile と組み合わせています。

Turnstile は、Cloudflare で発行したサイトキーとシークレットキーを、管理画面の お問い合わせ → インテグレーション に貼るだけです。Contact Form 7 6.1 から標準で対応しているので、別のプラグインは要りません。

日本語チェック は、題名と本文にひらがな・カタカナが1文字もなければエラーにする、というものです。Code Snippets プラグインで次のコードを追加しました。

add_filter( 'wpcf7_validate', function ( $result, $tags ) {
    $submission = WPCF7_Submission::get_instance();
    if ( ! $submission ) {
        return $result;
    }

    $text = $submission->get_posted_string( 'your-subject' ) . $submission->get_posted_string( 'your-message' );

    if ( ! preg_match( '/[\p{Hiragana}\p{Katakana}]/u', $text ) ) {
        $result->invalidate( 'your-message', '日本語を含めて入力してください。' );
    }
    return $result;
}, 10, 2 );

保存が 403 になるときは、サーバーの WAF がコードの中の文字列に反応している可能性があります。私の環境では、$_POST で入力値を取る書き方が引っかかっていました。上のコードのように Contact Form 7 の get_posted_string() で取ると通ります。

確かめた環境

  • WordPress(テーマは SWELL)
  • Contact Form 7 6.1.6
  • WP Armour – Honeypot Anti Spam 2.4.02
  • Code Snippets 3.10.2
  • サーバーの WAF:SiteGuard Lite

Turnstile を入れる

Turnstile は Cloudflare の CAPTCHA の代わりになるサービスです。ウィジェット作成画面の説明では、Managed モードの場合、ほとんどの訪問者は操作なしか見えないチェックで済み、リスクが高いと判断されたときだけ追加のチェックが出ます。

Cloudflare のドキュメントによると、2026年10月12日現在、無料プランで1アカウント20ウィジェット・1ウィジェット10ホスト名まで使え、チャレンジの回数には上限がありません。サイトを Cloudflare に載せていなくても使えます。

Cloudflare でウィジェットを作る

Cloudflare のダッシュボードで「Turnstile」を開き、「Add widget manually」からウィジェットを作ります。

  • Widget name:あとで見分けられる名前(私はドメイン名にしました)
  • Hostnames:フォームを置いているサイトのドメイン
  • Widget Mode:Managed(画面にも「Recommended」と出ています)
Cloudflare の Turnstile の「Add Widget」画面。Widget name と Hostnames に poco-tech.blog を入れ、Widget Mode は Recommended の Managed を選んでいる
Cloudflare の Turnstile の「Add Widget」画面。Widget name と Hostnames に poco-tech.blog を入れ、Widget Mode は Recommended の Managed を選んでいる

作成すると、サイトキーとシークレットキーが出ます。

Contact Form 7 にキーを貼る

WordPress の管理画面で お問い合わせ → インテグレーション を開きます。Turnstile の欄の「インテグレーションのセットアップ」を押すと、キーの入力欄が出ます。

Contact Form 7 のインテグレーション画面の Turnstile の欄。サイトキーとシークレットキーの入力欄と「変更を保存」ボタン
Contact Form 7 のインテグレーション画面の Turnstile の欄。サイトキーとシークレットキーの入力欄と「変更を保存」ボタン

2つのキーを貼って「変更を保存」を押せば終わりです。

フォームのテンプレートに [turnstile] タグを書く必要はありません。Contact Form 7 のソースを読むと、キーが登録されていて、フォームに [turnstile] タグがなければ、フォームの先頭に自動で入れるようになっています。好きな位置に置きたいときだけ、テンプレートに [turnstile] を書きます。

お問い合わせのページを開くと、フォームの上に Turnstile の枠が出ていました。

お問い合わせフォームの先頭に Cloudflare の Turnstile が表示され「成功しました!」になっている。フォームの下には、管理者にだけ見える WP Armour のハニーポットの案内が出ている
お問い合わせフォームの先頭に Cloudflare の Turnstile が表示され「成功しました!」になっている。フォームの下には、管理者にだけ見える WP Armour のハニーポットの案内が出ている

ログインしていないブラウザ(シークレットウィンドウ)で1通送ってみて、普通に送れることを確かめました。

ハニーポット(WP Armour)

上の画像の一番下にある青い枠は、WP Armour というハニーポットのプラグインの案内です。「管理者にだけ見える」と書かれているとおり、読者には表示されません。

ハニーポットは、人には見えない入力欄を置いて、そこに何か入っていたらボットとして捨てる仕組みです。このプラグインは前から入れていましたが、それでも迷惑メールは届いていました。ハニーポットだけでは足りなかったので、Turnstile と日本語チェックを足しています。

日本語チェックを入れる

このブログは日本語で書いているので、お問い合わせも日本語で来るはずです。そこで、題名と本文のどちらにも日本語がない送信はエラーにしました。

「日本語がある」の判定は、ひらがなかカタカナが1文字以上あるか にしています。漢字も含めると、中国語だけの文章も通ってしまうからです。

Code Snippets でコードを足す

PHP を足す場所は、Code Snippets プラグインにしました。テーマの functions.php に直接書くと、テーマを更新したときに消えることがあります。Code Snippets なら、管理画面でスニペットごとにオン・オフでき、テーマを変えても残ります。

Code Snippets は WordPress の公式ディレクトリにあり、有効インストール数は100万以上です。

  1. プラグイン → 新規追加 で「Code Snippets」を検索して、インストール・有効化
  2. 管理画面の スニペット の一覧で「スニペットを追加」
  3. タイトルを入れて、コード欄に上の「解決方法」のコードを貼る(先頭の <?php は書かない)
  4. 場所は「すべての場所で実行」のまま、「保存して有効化する」

保存しようとしたら 403 になった

最初は、入力値を $_POST から取るコードを書いていました。これを保存しようとしたところ、「スニペットを作成できませんでした。Request failed with status code 403」と出て保存できませんでした。

Code Snippets の新規スニペット画面。最初に書いたコードを貼って「保存して有効化する」を押したところ、右下に「スニペットを作成できませんでした。Request failed with status code 403」と出ている
Code Snippets の新規スニペット画面。最初に書いたコードを貼って「保存して有効化する」を押したところ、右下に「スニペットを作成できませんでした。Request failed with status code 403」と出ている

このサーバーは WAF(SiteGuard Lite)が有効で、以前にもブログの投稿が 403 になったことがあります。Code Snippets は保存のときにコードを REST API で送るので、その中身に WAF が反応しているのではないか、と考えました。

どの文字列が引っかかっているかを切り分ける

切り分けには、存在しない REST API のパスに、コードの一部だけを本文にして POST する 方法を使いました。

  • WAF が止めた場合:403 が返り、「閲覧できません (Forbidden access)」と「Powered by SiteGuard Lite」の書かれたページが出る
  • WAF を通った場合:WordPress まで届くので、パスがないことを表す 404(rest_no_route)が返る

ログインも要らず、存在しないパスなのでサイトには何も書き込まれません。

curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'Content-Type: application/json' \
  --data @body.json \
  https://(自分のサイト)/wp-json/code-snippets/v1/probe-nonexistent

body.json の中身を変えながら送った結果です。

本文に入れたもの 結果
コード全体 403
$_POST を使っていた2行 403
それ以外の行(1行ずつ) 404(通る)
isset( / wp_unslash( / preg_match( / add_filter( 404(通る)
$_POST だけ、$_POST のあとに [ だけ 404(通る)
$_POST のあとに、角かっこの添字('x' など)が続く形 403
上の形の $ や ' を HTML 実体参照・JSON のエスケープに変えたもの 403

$_POST に添字を付けて値を取り出す形が引っかかっていました。記号をエスケープしても止まったので、WAF はエスケープを戻してから中身を見ているようです。

ちなみに、この記事も REST API で投稿しているので、その形を本文に書くと投稿が 403 になります。最初のコードを文字ではなく画像で載せているのは、そのためです。

Contact Form 7 の関数で入力値を取る

$_POST を使わずに、Contact Form 7 の WPCF7_Submission から入力値を取るように書き換えました。それが「解決方法」に載せたコードです。

  • wpcf7_validate は、送信された内容をチェックするときに呼ばれるフィルターです
  • WPCF7_Submission::get_instance() で、いま処理している送信が取れます。Contact Form 7 6.1.6 のソースで、入力のチェックが走る時点ですでに取れることを確かめました
  • get_posted_string( 'your-subject' ) で、題名の入力値が文字列で返ってきます

書き換えたコードは、WAF にも止められずに保存できました。

Code Snippets の画面。get_posted_string で入力値を取るように書き換えたコードが入っている
Code Snippets の画面。get_posted_string で入力値を取るように書き換えたコードが入っている

動きを確かめる

シークレットウィンドウで、題名も本文も英語だけにして送ってみました。本文の下に「日本語を含めて入力してください。」と出て、送信されませんでした。日本語で送ったときは、今までどおり送信できました。

お問い合わせフォームで、題名「hello」・本文「this is the test」と英語だけで送ろうとしたところ、本文の下に「日本語を含めて入力してください。」と出て送信が止まっている
お問い合わせフォームで、題名「hello」・本文「this is the test」と英語だけで送ろうとしたところ、本文の下に「日本語を含めて入力してください。」と出て送信が止まっている

このコードを使うときの注意

  • フィールド名(your-subject / your-message)は、Contact Form 7 の初期のフォームの名前です。フォームを作り直して名前を変えている場合は、自分のフォームに合わせて書き換えてください
  • このフィルターは、サイトにある すべての Contact Form 7 のフォーム にかかります。your-subject も your-message もないフォームでは、調べる文字列が空になるので、毎回エラーになります。フォームを複数置いている場合は、フォームの ID で分けるなどの工夫が要ります
  • 英語だけのお問い合わせは、本物でも止まります。エラーの文も日本語なので、海外から連絡を受けたいサイトには向かないと思います

まだやっていないこと:コメント禁止ワード

Contact Form 7 は、WordPress の 設定 → ディスカッション → コメント禁止ワード に入れた言葉を含む送信も、スパムとして弾きます。ソースを読むと、入力した全項目と、送り主の IP アドレス・User-Agent をつなげた文字列に対して、大文字・小文字を区別しない部分一致で調べていました。

部分一致なので、短い一般的な言葉を入れると普通のお問い合わせまで弾いてしまいます。今回は入れていません。しばらく様子を見て、それでも届く迷惑メールがあれば、その中によく出てくる URL のドメインなどを入れようと思っています。

まとめ

  • Turnstile は Contact Form 7 6.1 から標準で使える。Cloudflare でキーを発行して、インテグレーションの画面に貼るだけ
  • 日本語チェックは、ひらがな・カタカナの有無で判定。Code Snippets で数行足す
  • Code Snippets の保存が 403 になったら、サーバーの WAF を疑う。存在しない REST のパスに本文だけ送ると、403 か 404 かで原因の文字列を絞り込める
  • 私の環境(SiteGuard Lite)では、$_POST に添字を付ける形が止められた。Contact Form 7 の get_posted_string() で入力値を取れば避けられる

迷惑メールがどのくらい減ったかは、しばらく使ってみて、分かったことがあればまた書いてみようと思います。

今回はハニーポットや日本語チェックを、既存のプラグインと数行のコードで入れました。このくらいの機能なら、今は Claude Code で小さなプラグインとして自作することもできそうです。今度トライしてみようと思っています。

参考:

Contact Form 7 のスパム対策:Turnstile と「日本語チェック」を入れる。Code Snippets の保存が 403 になったときの回避も

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

エンジニア。Claude Code / Codex / MCP / BigQuery / 自動化 / ガジェット / 3Dプリンター / スマートホーム。楽して生きたい。ゲーム / イヤホン / キーボード / プログラミング / AI

目次