PukiWikiのスパム対策akismet2.inc.php

PukiWikiを長く運営していると、スパムとの戦いは避けて通れません。ぼくのサイトでは、sonots氏が公開していたakismet.inc.phpをベースに、キャプチャ認証をGoogle reCAPTCHA v2に差し替えるなどの改修を加えながら使ってきました*1。Akismetで投稿内容をチェックし、スパム判定された場合はreCAPTCHAで人間確認を求める二段構えです。これで何年も持ちこたえてきたのですが、reCAPTCHAの無料枠が縮小されたことやGoogle Cloudアカウントが必須になったことから、キャプチャ部分を置き換える必要があるサイトも出てきているかと思います。今回、reCAPTCHAをCloudflare Turnstileに差し替えたakismet2.inc.phpを作りました。

- なぜreCAPTCHAをやめることにしたのか
- Akismet+Turnstileの二段構え
- Turnstileのサーバ側検証
- 旧版で放置されていた問題
- キャプチャ後に入力内容が消える問題
- 旧版との共存
- どこまでスパムフィルタするか問題
- 開発の進め方
なぜreCAPTCHAをやめることにしたのか
きっかけはGoogleからの料金体系変更の通知でした。reCAPTCHA v2は長らく無料で使えていましたが、Enterprise版への統合が進む中で無料枠が月10,000件まで縮小されています。ぼくのサイト程度のアクセス数なら枠内に収まるかもしれませんが、スパムボットが大量にPOSTしてくるとキャプチャ検証の回数はすぐに膨れ上がります。そもそもスパム対策のために課金が必要になるというのは、個人運営のWikiにはなかなかつらいものがあります。
代わりに選んだのがCloudflareのTurnstileです。規模を問わず無料で回数制限もなく、Cookieを使わないためプライバシー面での説明も楽になります。サイトをCloudflare経由にする必要もありません。Cloudflareのアカウントを作ってダッシュボードの「Turnstile」からドメインを登録するだけで、サイトキーとシークレットキーが手に入ります。reCAPTCHA v3のようなスコア閾値のチューニングもなく、設定はキー2つだけで済みます。
Akismet+Turnstileの二段構え
プラグインの基本構造は旧版と同じです。PukiWikiのlib/pukiwiki.phpにフックを1箇所入れて、すべてのPOSTリクエストをspamfilter()に通します。
if (exist_plugin('akismet2')) {
PluginAkismet2::spamfilter();
}
フィルタの流れはこうです。まずAkismetのAPIに投稿内容を送り、スパムかどうかを判定してもらいます。Akismetが「スパムではない」と返せばそのまま通過します。「スパムだ」と返した場合、Turnstileのキャプチャフォームを表示して人間確認を求めます。Turnstileを通過すれば投稿は受理され、同時にAkismetへ「これはスパムではなかった」と取り消し報告を送ります。セッションに認証済みフラグを立てるので、同じ訪問者が2回目以降に投稿するときはキャプチャなしで通ります。
Akismetを使わずTurnstileだけで運用することもできます。PLUGIN_AKISMET2_USE_AKISMETをFALSEにすると、すべての投稿をスパムとみなしてキャプチャ認証を毎回求める形になります。Turnstileをオフにすることもできます。その場合、キャプチャ課題の代わりにボタンだけが表示され、押せばham報告と投稿が進みます。ただ、ボットにも押せてしまうので、実運用ではTurnstileを有効にしておくのが無難です。
Turnstileのサーバ側検証
Turnstileの検証はシンプルです。reCAPTCHA v2ではGoogleが提供するPHPライブラリを同梱する必要がありましたが、TurnstileはPHP標準のcurlだけで完結します。フォーム送信時にブラウザからcf-turnstile-responseトークンが飛んでくるので、それをCloudflareのSiteverify API(<a href="https://challenges.cloudflare.com/turnstile/v0/siteverify" rel="nofollow">https://challenges.cloudflare.com/turnstile/v0/siteverify</a>)にシークレットキーと一緒にPOSTし、返ってきたJSONのsuccessがtrueかどうかを見るだけです。コードはむしろ旧版より短くなっています。
Turnstileのapi.jsはキャプチャフォームのHTML内で読み込んでいるので、スキンファイルを編集する必要はありません。旧版ではreCAPTCHAのapi.jsをスキンに埋め込む手順がありましたが、それも不要になりました。
旧版で放置されていた問題
reCAPTCHAからの乗り換えをきっかけに、旧版のコードを読み直してみると、気になる箇所がいくつも見つかりました。せっかく書き直すなら一緒に直してしまおうと、結果的にかなり手を入れています。
一番まずかったのがスパムログ閲覧機能のセキュリティです。旧版ではlogfileパラメータに任意のパスを渡せたため、サーバ上のファイルが読み出せる状態でした。反射型XSSの問題もありました。akismet2ではlogfileパラメータをホワイトリスト方式に変更し、スパムログのローテーションファイル以外は指定できないようにしています。ログ閲覧自体もデフォルトで管理者限定にしました。ログにはIPアドレスやUser Agentが含まれるので、誰でも見られる状態はそもそもよくありません。
Akismet APIとの通信もHTTP(80番ポート)からHTTPS(443番ポート)に変更しました。APIキーをPOSTで送っているわけですから、平文で流すのは避けるべきです。接続先も現行の公式ドキュメントに合わせてrest.akismet.comに固定し、APIキーはサブドメインではなくapi_keyパラメータとしてPOST本文で送る形式にしました。旧版のreCAPTCHA検証通信ではSSL証明書の検証を無効化していましたが、これも有効化しています。
PHP 8.5対応も必要でした。strftimeが廃止予定になっている件とsession_startの多重呼び出しでWarningが出る件に対処しています。
キャプチャ後に入力内容が消える問題
旧版で地味に困っていたのが、キャプチャ認証を通過したあとにトップページに飛ばされて、書いた内容が消えてしまうことがある問題です。コメントを書いてスパム判定され、キャプチャを解いたら元のページではなくトップに戻されて、もう一度同じコメントを書き直す。これは実際にやられると結構こたえます。
原因はキャプチャフォームに元のプラグイン名を保存していなかったことでした。akismet2ではフォームに元のプラグイン名と入力データを保持しておき、認証後にそれを使って元の投稿処理に確実に戻します。万が一投稿先の特定に失敗した場合も、入力内容を保持した再投稿フォームを表示するようにしました。書いた内容が黙って消えることだけは避けたかったところです。
旧版との共存
akismet2.inc.phpはプラグイン名、クラス名(PluginAkismet2)、設定定数をすべて旧版と分離してあるので、同じpluginディレクトリに両方を置いても衝突しません。内蔵のAkismet通信クラス(AkismetObjectなど)は両版で同じクラス名を使っているため、二重定義ガードを入れてあります。ただし、lib/pukiwiki.phpのフックはどちらか一方だけにしてください。両方をフックすると同じ投稿が二重にスパム検閲されます。
旧版のakismet.inc.phpもv2.1.0で二重定義ガードと同様のセキュリティ修正を入れています。ただ、新規に導入するならakismet2を使ったほうが設定は楽です。reCAPTCHAのPHPライブラリを同梱する必要がなくなっているぶん、ファイル数も少なくて済みます。
どこまでスパムフィルタするか問題
Akismetに送る「本文」として、PukiWikiのどの変数をマッピングするかは悩みどころです。PukiWikiではプラグインごとに本文が格納される変数名が違います。commentプラグインは$vars['msg']ですが、trackerプラグインはユーザー設定次第です。
いくつかの案を検討しました。$vars['msg']だけ送る案はシンプルですが対応範囲が狭い。$vars全体を送る案はすべてのフィールドをカバーできますが、メッセージダイジェスト値のような自動生成値まで含まれてしまい、正常な投稿がスパム誤判定されないか不安が残ります。editプラグインの場合は$vars['original']に元の文章全体が入っているので、Akismetに送るデータが倍になる問題もあります。page_write関数の直前でチェックする案も試しましたが、初版では不具合が出やすいことがわかりました。
結局、$vars全体を送りつつ、個々のプラグインで不要な値を除外する方式に落ち着きました。完全に理想的とは言えませんが、実用上はこれで問題なく動いています。trackerのように変数名がユーザー設定依存のプラグインまで完全対応しようとすると、プラグインの仕様変更のたびに追従が必要になるので、ある程度の割り切りは必要でした。
開発の進め方
今回もClaude Codeと一緒に開発を進めました。旧版のakismet.inc.phpはAkismet通信、reCAPTCHA検証、スパムログ、セッション管理、キャプチャフォーム生成と、やっていることが多岐にわたります。ぼくが「reCAPTCHA部分をTurnstileに差し替えたい」という方針を出し、AIがコードの構造を把握したうえで差し替え箇所を特定していく形で進めました。
セキュリティ周りの修正は、コードを読み直す過程で見つかったものがほとんどです。logfileパラメータの問題は、ぼくが手動でテストしていたら見落としていたかもしれません。ただ、PukiWikiの認証周りの仕様判断(PukiWiki本家ではユーザー名'admin'を管理者とみなす、といった慣習)は、運用経験がないとわからない部分です。このあたりの判断はやはり人間の仕事でした。
プラグインのソースとインストール手順はakismet2.inc.phpに置いてあります。動作にはPHPのcurl拡張が必要ですが、一般的なレンタルサーバーでは有効になっています。reCAPTCHA版からの移行は、lib/pukiwiki.phpのフック行を書き換えてakismet2.inc.phpにキーを設定するだけです。Akismetのスパム学習データはそのまま引き継がれるので、乗り換え直後から従来どおりの精度で動きます。

この記事に対するコメント
このページには、まだコメントはありません。
更新日:2026-06-13 閲覧数:24 views.