最近ようやく初級プログラマーを卒業できた手応えのようなものを感じており、いよいよコードを読み書きするのが楽しくてしょうがない段階になってまいりました。

こういうとき、Rubyは初心者にもやさしいけど、上達すればどこまでも上のステージが用意されているような、まるで自然言語のようななめらかさ・しなやかさがあって、ほれぼれとします。デザインの美しいものに触れているときには人間はこんなにも幸せになれるのか、という感じですね。ときに、今回のブログネタは、デザインの悪いものに出会うとこんなにも気分が悪くなるのか、という話なのですが。

なお、新プロジェクトではデザイナーのクリスの勧めでHamlを使うことにしたり、アーキテクトのダニーの設計でJavascriptにPublish-Subscribe型の（つまり一対多の）コールバックのフレームワークを作ってみたり、ReallySimpleHistoryを使い、Ajaxでも「戻る」ボタンを使えるようにしたり、色々と新しいことに取り組んでいます。まだちょっと時間がかかってますが、春には面白いものを出せると思いますのでお楽しみに。

さて、そんなわけで毎日ひたすらコード書いてるとブログ書くネタがなくなるわけですが、先ほどたまたまこんな記事を見かけました。

ITmedia: 変形文字「CAPTCHA」はもう無意味？

最近、機会あるごとにCAPTCHAはダメだということを強調し続けているのですが、この際きちんとブログに意見表明しておこうかと。

CAPTCHAの抱える問題とは、ようするに「強度を上げれば（読みにくくすれば）人間にも読みにくい」という単純かつ原理的な矛盾です。

本来、CAPTCHAはボットによる自動アカウント取得などを阻止する手段として、「ユーザにほんの少しだけ協力してもらう」というスタンスから出発しました。逆にいえば、これは100%サービス提供側の都合であって、ユーザにとってはどうでもいいことなのです。

ところが、自分の都合であるということを忘れたサービス提供者は調子に乗ってどんどん強度を上げ、偽陽性にイライラするユーザへの配慮を忘れ、まるでこれは「ユーザの当然の義務」であるかのようにふるまいはじめます。

これは、サービスを「おもてなし」と捉える立場からは考えられない態度です。

最近ではとうとうGoogle検索でさえCAPTCHAを見るようになってきました。特定の状況下でこれがしょっちゅう出てイライラします。

まぁ、逆に考えれば、グーグルですらこの程度の対策しかできないのか、というのはかえって自信にもなりますが。

以下は「では、他にどういう方法があるの？」という話。

結論から言うと、低い実装コストで、ほとんどのボットを排除でき、かつ正規ユーザの手を一切わずらわせない方法というのは、割とあると思います。

いくつかポイントがあります。

Javascriptを使う サーバだけが知る秘密情報を使う Javascriptを難読化 クライアント側の処理をサーバよりも高コストにする

上記は、コストパフォーマンスの高いものから順に挙げています。

まず、一番簡単で効果の高い「Javascriptを使う」について。

ターゲットとなるフォームに、以下のようなhiddenフィールドを追加して、



<input id="token" type="hidden" />

このフィールドにJavascriptを使って動的に値をセットします。



<script type="text/javascript"> document.getElementById('token').value = "Some secret" </script>

そしてサーバ側ではこれがちゃんと送られているか判定。

これだけで、８割方の粗雑なボットは排除できます。CAPTCHAよりも何よりも、まずはこれをやってみましょう。

しかし、このままではルールが静的なので、その気になれば一瞬で対応できてしまいます。

そこで、次に「サーバだけが知る秘密情報」を使って動的にします。ごく簡単な例として、ここでは時刻を用います。



<script type="text/javascript"> document.getElementById('token').value = 'Thu Feb 28 00:06:14 -0800 2008' </script>

この 'Thu Feb 28 00:06:14 -0800 2008' というのは Ruby の Time.now の文字列表現そのままです。これを受け取るサーバ側では（以下Railsの例）



def verify_token t = Time.parse(params[:token]) rescue Time.gm(0) Time.now.between?(t, t + 2.hours) end

という感じのバリデータ（この場合、フォームが表示されてから2時間以内のsubmitしか有効でないとしています）を用意しておけば、夜間バッチ型のボットを防げるようになります。

さらに値をSHA-1などでハッシュしてしまえば、ハッシュする前のデータが推測できず、リプレイ攻撃以外に手段がなくなります。

そうしたら次の段階は自明ですね。リプレイ攻撃を防ぐには、同じ値を二度使えないようにすればいいのです。

ただ、ワンタイムなnonceの管理は理屈は単純ですが実装コストが高いので、ここでは後回しにします。

次に検討したいのはJavascriptの難読化です。たとえば、私のこのブログのプロフィールページにメールアドレスが書いてありますが、実はあれはHTML上にテキストとしては書いておらず、Javascriptで組み立てて生成しており、ボットからは発掘しにくくなっています。

ソースはこんな感じ。



<span id="placeholder"></span> <script type="text/javascript"> var a = document.createElement("a"); var b = "golb_nnek".match(/./g).reverse().join("")+String.fromCharCode(0x40)+ "japan.cnet"+String.fromCharCode(46)+"com"; a.href = ":otliam".match(/./g).reverse().join("")+b; a.appendChild(document.createTextNode(b)); document.getElementById("placeholder").appendChild(a); </script>

読めば何やってるか理解できると思いますが、このスクリプトを実行した結果、生成され、離れたspanタグにDOMで挿入された内容がメールアドレスであるとボットが判定するのは結構大変です。この例はかなり平易ですが、evalを使ったり、工夫次第でいろいろと（ボットやボット作者への）意地悪ができるはずです。

よくあるメールアドレスを画像で表示するのと違って、ブラウザで表示すれば普通にクリックもできるしコピー＆ペーストもできますから、訪問者にやさしい。

これと同じアイデアをCAPTCHA的な用途にも応用できるでしょう。

しかし、究極的にはボットはブラウザの全ての機能を完全に再現する、というか、ブラウザのエンジンをそのまま使うようになるでしょう。こうなると、どんなJavascriptも解読されてしまうし、ブラウザを操作してる人間とまったく見分けがつきません。

しかし、CAPTCHAのようなユーザの手をわずらわせるアプローチに走る前に、まだやれることがあるはずです。

それが最後の「クライアント側の処理を高コストにする」です。

基本的な考え方はHashcashやTarpitと似ていますが、スパマーも一定の経済原則に沿って行動しているという事実を逆手にとる考え方です。

つまり、Hashcashのように、一件処理するのに必要なクライアントサイドでのCPUの利用率を意図的に高めることでスパマーにとっての実行コストを上げたり、Tarpitのように、サーバサイドで意図的なI/O遅延を発生させてスループットを悪化させたりするということです。（後者はCometと同じく非同期で動作するイベント・ドリブンなウェブサーバが必要になるので簡単ではありませんが）

これにはもちろん、「ユーザレベルでは遅いとは体感することができない」という制約条件が必要ですが、トータルでのコストを高めるのが目的なので、ユーザ一人あたりに乗せるコスト単価はそれほど大きくなくてもいいのです。

そもそも実行時リソースの要求が高いフルブラウザベースのボットを作り込むようなハイエンドなスパマーはコスト感覚にシビアなはずなので、かなり効果があるのではないかと思われます。

たとえば、上記コードの難読化にprototype.jsなどのライブラリのコードを積極的に使うのも一例です。外部jsファイルのインクルードはボットにとってもコストが高い処理なので、より高い排除効果が期待できます。

この「クライアント側の処理を高コストにする」を達成する具体的な方法は他にも色々考えられると思いますので、どなたかいいアイデアを思いついたらぜひ教えてください。

さて、あれこれ書いてきましたが、一貫しているのは「ユーザ（人間）に負荷を与えるデザインは絶対にダメ」ということです。そこに手をつける前に、やれることはたくさんあるでしょう？ということが言いたかったのです。

うーんやっぱり長くなってしまった。

♪ Marc Broussard / Rocksteady