Dev Stories

魔法使い「Honey Potter」から侍へ…【Samurai Honeypot】開発ストーリー

· 1 min read

すべてのプロジェクトにはコードネームがある。つまらないものもあれば、恥ずかしいものもある。私たちのは後者だった。

「Honey Potter」 それが初期コードネームだ。

そう、あの有名な魔法使いである。決して誇りには思っていない。この名前はSlackでの雑談から始まった——誰であろうこの私が「honeypot」と「Harry Potter」をくっつけたら、それがそのまま定着してしまったのだ。気がつけば、Gitのブランチは honey-potter/feature-xyz になり、Slackチャンネルは #honey-potter-dev になり、社内ドキュメントのマイルストーンには杖の絵文字(🪄)がついていた。

しかし、実はそのメタファーはかなり的確だった。私たちは魔法のようなものを作りたかったのだ。シンプルな呪文——ハニーポット——で、誰にも気づかれずにスパムを消し去る。杖を振り、呪文を唱えれば、問題解決。

当時は気づいていなかったが、現実の「闇の魔術(スパムボット)」はかなりレベルアップしていた。1年生のシンプルな呪文では太刀打ちできなかったのだ。

これは Samurai Honeypot for Formsプラグイン開発ストーリー——冗談のようなコードネームが本格的なアンチスパムエンジンになるまでの過程と、その途中で私たちが犯した「誤った前提」の全記録である。


誕生の経緯

始まりは一本の電話だった

月曜の朝だった。始業と同時に電話が鳴った。

電話の相手はクライアントだった。「今週の問い合わせ、半分がスパムでした。またか、という感じです……」正当な問い合わせのほぼ全てが日本国内からのものだったが、スパムの99%は海外からだった。外国語のメッセージ、海外のIPアドレス、何もかもが国外。

まず手っ取り早いことを試した。「日本で生活していれば答えられるクイズ」をフォームに追加したのだ。効果は劇的で、スパムは一夜にしてほぼゼロになった。しかしクライアントのマーケティングチームは難色を示した。洗練されたお問い合わせフォームの途中に突然のトリビア質問——UXが台無しだ。それに、LLM(大規模言語モデル)が月単位で賢くなっている今、このクイズにも有効期限があることは分かっていた。

そこでより広いエコシステムを調べた。まずは王道の reCAPTCHA。v3は多少効果があったが、VPNを使用している正規ユーザーを誤検知してしまった。Akismet は一部を検出したが、賢いAIスパムは見逃した。さらに私たちは、高いデータプライバシー基準を保つため、送信のたびにサードパーティのAPIにデータを送るような仕組みは避けたかった。シンプルなハニーポットプラグインも試したが、すぐにボットが「隠しフィールドをスキップすること」を学習した。

これらを組み合わせれば防げるかもしれない。しかし、コンタクトフォームを守るためだけに複数プラグインを同時に動かし、互いの競合に怯えながら保守する負担は馬鹿げていた。何をやっても、別の問題(パフォーマンス低下やUXの悪化、保守コスト)を生み出していた。

そこで、既存のツールに失望したときに開発者が必ずやることを、私たちもやった。「自分たちで作ってみよう」と思い立ったのだ。サードパーティのツールをツギハギすることにうんざりしていた私たちは、純粋なエンジニアとしての好奇心から、新しいリポジトリを開いた。

「ただのハニーポット」フェーズ

Honey Potterの最初のバージョンは、まさにその名前通りだった。ただのハニーポット。1つの隠しフィールドを Contact Form 7 のフォームに注入し、サーバーサイドでチェックする。フィールドに値が入っていれば、送信を拒否。古典的なアプローチだ。

午後の数時間で書き上げた。PHPで80行程度だった。テストサイトにデプロイし、全体的なスパムの量が劇的に減ったのを見て、私たちは自分たちの賢さに満足した。

しかし、私たちが「これで完璧だ」と大人しくしていられたのはわずか11日間だった。

誤解のないように言うと、11日目にプラグインが破られてスパムが激増したわけではない。単純なスクリプトによる大量送信は引き続きしっかり防げており、スパムの絶対量は明確に減っていた。

では、何が起きたのか? それは、ログを見つめるうちに「このすり抜けてくる賢いスパムも、どうにかして全部捕まえたい!」という、エンジニアとしての欲求に火がついてしまったのだ。

ログを分析すると、ヘッドレスブラウザ(実際の Chromium インスタンスを実行しているボット)は、最初から私たちのハニーポットなど存在しないかのように素通りしていることが分かった。彼らは getComputedStyle をチェックして隠しフィールドをスキップし、悠々とスパムを送信してきていた。「量は減らせた。でも、このすり抜けてくるヤツらも、やり方次第でもっと減らせるんじゃないか!?」

ただのハニーポットの効果で満足していれば、そこで話は終わっていたはずだ。しかし、この「もっと最適化して、完璧にシャットアウトしてみたい」という技術的な探求心が、私たちを次なるフェーズへと突き動かした。

過剰設計フェーズ

「一滴残らずスパムを根絶やしにする」という欲求に駆られた私たちは、「機能を更に盛り込む」というありがちな罠に嬉々として飛び込んでしまった。

2番目のプロトタイプには、機械学習スコアリング、Canvasフィンガープリンティング、ボットのシグネチャを追跡するローカルSQLiteデータベースを搭載した。賢いボットに勝つには、さらに賢いシステムが必要だと確信していた。

テスト環境では見事に動いた。しかし本番環境では悪夢だった。MLモデルには持ち合わせていない学習データが必要だった。フィンガープリンティングはSafariで壊れた。SQLiteデータベースは静かに肥大化し、ディスクスペースを食い潰す未来が見えた。何かが壊れたとき——そしてそれは頻繁に起きた——デバッグのために3つの異なるサブシステムを同時に理解する必要があった。

私たちはそれを破棄して基本に立ち返った。教訓は明確だった。最高のセキュリティツールは、中身が退屈(シンプル)なものだ。 退屈なコードは信頼できるコードだ。賢さはアーキテクチャに使え。実装はストレートに保て。


現実の直視

シンプルな魔法では足りない

認めなければならない不都合な真実があった。従来のハニーポットは単体では、すでに攻略済みの手法だ。 私たちが攻略したのではない——ボットが攻略したのだ。

2015年に有効だったCSSトリック——display: noneopacity: 0、画面外への配置——はすべて、モダンなボットにとっては1行のJavaScriptチェックでしかない。フィールド名は固定、隠蔽方法は固定、DOMでの位置も固定。ボットの作者がフォームを一度リバースエンジニアリングすれば、永久に使えるバイパスが書ける。世界中の多くのサイトが、これと同じような無力な対策に頼っているはずだ。

私たちは、7年生の闇の魔法使いに対して、1年生の呪文を唱えていたのだ。(すみません、この時はまだまだ Honey Potter の設定を引きずっていました)

問題はハニーポットというコンセプトそのものではなかった。問題は、私たちのハニーポットが予測可能だったことだ。毎回のページロードで同じフィールド、同じ名前、同じ隠蔽方法。鍵を作って、その合鍵をご丁寧に配り歩いていたようなものだった。

すべてが変わった瞬間

転機は、テストサイトでロギング実験を実施したときに訪れた。フォームに計測を仕込み、すべてを記録した。送信タイミング、フィールドの入力順序、JavaScriptが実行されたかどうか、マウスイベントが発生したかどうか、あらゆるデータだ。

1週間後、大量の送信データが得られた。そしてパターンは衝撃的だった:

  • 87%のスパム送信が2秒以内に完了していた。人間なら15〜45秒はかかる。
  • 94%のスパムにはマウス移動イベントがゼロだった。キーボードのみのユーザーであっても、モバイルなら何らかのポインター操作が発生するはずだ。
  • ハニーポットをバイパスしたスパムの100%が、フィールド入力前にCSSを評価するボットからのものだった。
  • スパムの増加する一部は、文法的に正確で、文脈に沿っており、明らかにAI言語モデルによって生成されたものだった。

そのデータが、問題への考え方を根本的に変えた。対処すべきは1種類の攻撃者ではなかった。単純なcurlスクリプトからAI搭載エージェントまでの幅広いスペクトラム(多様な層)に対処しなければならず、単一の防御レイヤーではその全範囲をカバーできなかったのだ。

私たちに必要なのは、より強力な呪文ではなく、悪意を能動的に切り捨てる「刀」だったのだ。

呪文から「刀」へ

ここで Honey Potter のメタファーは完全に終わりを告げる。ハニーポットは「罠」だ。受動的なもので、ボットがミスをするのを待つ。しかし、ボットがそのミスを犯さなかったら?

私たちが作ろうとしていたのは、単に身を守るだけの受動的な盾ですらなかった。すべてのリクエストを能動的に検証し、複数のシグナルをチェックして、クライアント側からの情報を一切信頼しない。少しでも人間離れした挙動を見せれば、容赦なく切り捨てる。そう、私たちが鍛え上げていたのは「刀」だったのだ。

単一トリックの罠から、多層防御システムへの移行。以下が、私たちが一つずつ追加していった太刀筋(レイヤー)だ:

レイヤー1:ポリモーフィック・ハニーポット。 1つの固定的な隠しフィールドの代わりに、ページロードのたびに「ランダム化された名前」と「ランダム化されたCSS隠蔽手法」を持つフィールドを生成する。形を常に変えるその構えは、攻撃者に研究する隙を与えない。

レイヤー2:タイミング分析。 フォームのレンダリング時に署名付きタイムスタンプを埋め込み、送信時に経過時間をチェックする。3秒以下はほぼ確実に自動化ツールだ。実装は極めてシンプルだが、驚くほど多くのスパムを捕捉する。

レイヤー3:クライアントサイド Proof of Work。 フォーム送信前にブラウザが小さな計算パズルを解く必要がある。人間のブラウザでは数百ミリ秒で終わるためユーザーは気づかないが、数千件のフォームを送信しようとするボットにとっては、その計算コストが急速に積み上がり割に合わなくなる。

レイヤー4:ステートレスHMACトークン検証。 すべてのフォームにサーバーサイドで生成された暗号トークンを付与し、トークンなしでは送信不可にする。トークンはステートレス(DBなし、セッションなし)なので、強力なページキャッシュやCDN環境下でも壊れずに動作する。

レイヤー5:サイレント拒否。 ボットを検出しても、相手には何も教えない。フェイクの「送信成功メッセージ」を返す。ボットは勝ったと思い込み、先に進む。フィードバックループを断ち切ることで、ボットが適応するためのシグナルを奪う、いわば音無しの剣だ。

各レイヤーは単独では不完全だ。しかし組み合わさることで、互いの死角を補う。ハニーポットをスキップするほど賢いボットでもタイミングチェックで斬られる。人工的な遅延を加えるボットでも大規模には Proof of Work を処理しきれない。それらすべてをこなすボットでも、リクエストを偽造した場合は有効なHMACトークンを持たない。

多層防御は単なるバズワードではない。現実的に維持できる唯一の戦略なのだ。


成果

Honey Potter が最終的に成ったもの

あの気の抜けたコードネームは、最終的に Samurai Honeypot for Forms ——Contact Form 7 用の本番グレードのアンチスパムプラグイン——になった。

名前から魔法使いの要素は消え(私たちのマーケティングセンスは、ネーミングセンスよりは多少マシだったようだ)、代わりに侍が残った。その核心は今もハニーポットだが、受動的な壺から、毎回のページロードで適応する能動的な多層防御システムへと進化した。

プラグインの機能を平易にまとめるとこうなる:

  • ポリモーフィック・ハニーポット —— 動的な名前とランダム化されたCSS隠蔽。ボットにとって、2回のページロードが同じに見えることはない。
  • 送信タイミング検証 —— 人間には不可能な速さでフォームを入力するボットを検出。
  • クライアントサイド Proof of Work —— ユーザーには見えないが、大量送信ボットには高コストを強いる。
  • ステートレスHMACトークン —— CDNやページキャッシュの背後でも壊れずに動作。
  • サイレント拒否モード —— ボットに検出されたことを一切知らせない。
  • ゼロ設定。 インストール、有効化、以上。APIキー不要、アカウント登録不要、設定ページの調整も不要。
  • Cookie不使用、外部リクエスト不使用。 サードパーティにデータを送らず、設計段階からデータプライバシーを保護。
  • 完全なアクセシビリティ。 スクリーンリーダーやキーボードナビゲーションを妨げないよう、aria-hiddentabindex 属性で適切に処理。

開発から学んだこと

すべてのプラグイン開発ストーリーには教訓がある。私たちの教訓はこうだ。

前提ではなく、データから始めよう。 「ハニーポットで十分だ」と思い込んでいたのは、過去にハニーポットが十分だったからだ。ロギング実験が、その思い込みの間違いを教えてくれた。最初のバージョンで満足していたら、「低レベルなボットは防ぐが、本当に厄介なスパムには無力なプラグイン」をまた一つ世に出しただけだっただろう。

ボットのエコシステムはスペクトラムだ。 相手は単一の「ボット」ではない。curlスクリプト、ヘッドレスブラウザ、AIエージェントと同時に戦っている。1種類を阻止して他を無視する防御は、誤った安心感を与えるだけだ。

ステートレスは思った以上に重要だ。 トークンシステムの初期バージョンはWordPressの transients(一時データ)を使用していた。開発環境では動いたが、ページキャッシュを使用しているサイトでは即座に壊れた。ステートレスなHMACトークンへの移行で問題が完全に解決し、テストしたすべてのキャッシュ設定との互換性が確保された。

サイレント拒否は、アンチスパムにおいて最も過小評価されているテクニックだ。 403やエラーメッセージを返した瞬間に、私たちは攻撃者を「教育」していることになる。「ありがとうございます」メッセージ付きのフェイク200レスポンスは、ボットに「アプローチが機能しているかどうか」の情報をゼロしか与えない。

プラグイン開発はコードの問題だけでなく、ユーザー体験の問題だ。 ユーザーは複雑な設定ページなど見たくない。インストールしたら動くことを望んでいる。追加を検討したすべての設定について、「私たちが適切なデフォルトを選べないか?」と自問した。ほぼ毎回、答えはイエスだった。

コードネームは生き続ける

社内では今でもたまに Honey Potter と呼んでいる。古い習慣だ。Slackチャンネルはリネームされていないし、遡ればオリジナルのGitブランチプレフィックスも履歴に残っている。

それは原点を思い出させてくれる良いフックだ。スパムは単純な問題であり、単純な解決策があると思い上がっていたチームの記憶。その名前が、私たちを謙虚に保ってくれる。チームの誰かが新機能について「これだけで十分じゃないか?」と言うたびに、別の誰かが「ハニーポッターで十分だと思っていた頃を思い出せ」と返す。

そして私たちは、再び刀を研ぎに戻るのだ。


まとめ

  1. 従来のハニーポットは必要だが、十分ではない。 ボリュームの多数を占める低コストのボットは捕捉できるが、ヘッドレスブラウザを使う高度なボットには無力だ。

  2. 現代のボット防御には複数のレイヤーが必要だ。 ポリモーフィックフィールド、タイミングチェック、Proof of Work、暗号トークン、サイレント拒否——それぞれが他のレイヤーの穴を埋める。

  3. 合理的なデフォルトで出荷しよう。 最良のセキュリティプラグインとは、有効化した瞬間に機能するプラグインだ。過剰な設定項目は機能ではなく、障壁だ。

  4. 攻撃者にフィードバックを与えるな。 サイレント拒否は、ボットの進化を駆動するシグナルを奪い取る。

  5. データから始めよう。 ログを取り、計測し、分析する。フォームに何が到達しているかについてのあなたの「前提」は、おそらく間違っている。私たちのがそうだったように。

もしあなたが Contact Form 7 を使っていてスパムにうんざりしているなら、Samurai Honeypot for Forms を試してほしい。無料で、セットアップ不要で、「シンプルな魔法では足りない」ことを身をもって学んだチームが開発したものだ。

私たちはただ、この答えに辿り着くために「Honey Potterフェーズ」を通過する必要があっただけなのだ。

すべてのコラム