perlによる掲示板に画像パスワードを導入して,ツールによるスパムを防止する
1.はじめに良く知られたフリーのCGI(例えばケント等)を使って掲示板を動かしていると, そのうち海外からのスパム書き込みに見舞われるようになる.これは自動ツールを使っているようで,消しても消してもどんどん書き込まれる. 腹が立つのでIPアドレスでアクセス禁止しようとしても, たいていの場合はproxyを使っているのか書き込み元のIPアドレスがコロコロ変わるので, 禁止しようがない. このような自動ツールの書き込みを防ぐためによく使われているのが, 書き込み時に画像で表示されている文字を入力させる方法である. 正式には,「Captcha」と呼ばれているようだ.
カモランドでもそのような仕組みを導入したので詳細を解説する. ただし,RensaInfoに見られるような俺の趣味によって,玉の色を選択させる方式になっている.
2.画像Capthaを用いた掲示板の基本構造投稿ページは以下のような構成となる.パスワード画像を表示している箇所は,吹き出しで書いてある通り, <img src="/passimage.cgi?key=xxxyyyzzz">という風にCGIで画像を生成している. この例では「1352」という文字を表示させているが, このCGIに渡しているパラメータは1352ではなく別の文字列である. 「1352」そのものを渡すとパスワードが自動ツールにばれてしまうからだが,ここは重要なポイントである. それで投稿時に入力されたパスワードを検証するために, 投稿時には,投稿内容と一緒にその文字列もhiddenでPOSTする.
3.各処理の詳細投稿ページ投稿ページの表示時は,毎回パスワードをランダムに生成する.例えば以下のようなイメージ.
my(@plain) = (); for (1..4) { push(@plain, substr("123456", rand(6), 1)); } my($password) = join('', @plain); my($key) = myencode($password); 1〜6の文字が4個並んだ文字列を生成している. HTMLに埋め込むためのキーは,それをmyencode()で暗号化したものである. myencode()の中身はとりあえずここでは省略.
パスワード画像生成プログラムGETパラメータでキーを受け取るので,これを復号化して表示すべきパスワード文字列を 求め,それを画像として出力する. 今回は,表示すべき画像をPNGで用意しておいて, それをpngren.plを用いて連結した結果を出力するようにした.
#!/usr/bin/perl # パラメータから画像を生成する # passimage.cgi require './pngren.pl'; # 画像ファイルのパス $imgpath = '../image'; # キャッシュ化する画像のリスト。 @imgs = ("$imgpath/pg1.png", "$imgpath/pg2.png", "$imgpath/pg3.png", "$imgpath/pg4.png", "$imgpath/pg5.png", "$imgpath/pg6.png"); # キャッシュを埋め込む。 foreach(@imgs) { &pngren::CachePNG(\$_, $pngren::ALL24); } # これでキャッシュ化完了。 $buffer = $ENV{'QUERY_STRING'}; @pairs = split(/&/, $buffer); my($key) = ''; foreach (@pairs) { my($name,$value) = split(/=/); if ($name eq "key") { $key = $value; } } die if (length($key) < 8); # mydecode()は復号化の関数である.中身は略 my($password) = mydecode($key); # 復号化の結果は,1〜6が並んだものになる # 例:1352 my @im = (); foreach (split(//, $password)) { push(@im, $imgs[$_ - 1]); } # キャッシュしたものを使う。 &pngren::OutMIME('image/png'); &pngren::PngRen(\@im, $pngren::CACHEONLY); exit(0);
投稿処理プログラムPOSTパラメータとして,キーと入力されたパスワードの両方を受け取る. パスワードを暗号化して,キーと一致するかどうかをチェックすればよい. 以下のようなイメージである.
if (myencode($password) ne $key) { # パスワードが違うので投稿失敗 }
4.暗号化の方法画像生成時には,キーからパスワードを復号化する必要があるので,可逆の暗号方式を使う必要がある. 今回は,お手軽に使えそうなCrypt::RC4を使用した.
そして,キーとパスワードの組み合わせを収集して秘密鍵を類推されるのを避けるために, ランダムなワンタイムパスワードを毎回生成して,それを使うようにした.
#!/usr/bin/perl use strict; use Crypt::RC4; $::adminpass = 'himitu-password'; my($plain) = '1352'; my($enc) = myencode($plain); my($dec) = mydecode($enc); print "$plain\n"; print "$enc\n"; print "$dec\n"; exit(0); sub myencode { my($plain) = @_; my($keytable) = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; my(@onepass) = (); for (1..10) { push(@onepass, substr($keytable, rand(length($keytable)), 1)); } my($onetimepass) = join('', @onepass); my($enc_onetimepass) = RC4_hex($::adminpass, $onetimepass); my($k) = RC4_hex($onetimepass, $plain); return $k . $enc_onetimepass; } sub mydecode { my($enc) = @_; my($enc_onetimepass) = substr($enc, 8); my($k) = substr($enc, 0, 8); my($onetimepass) = RC4_dec_hex($::adminpass, $enc_onetimepass); my($plain) = RC4_dec_hex($onetimepass, $k); return $plain; } sub RC4_hex { my($pass, $plain) = @_; my($encbin) = RC4($pass, $plain); my(@enchex) = (); foreach (split(//,$encbin)) { push(@enchex, unpack("h2",$_)); } return join('',@enchex); } sub RC4_dec_hex { my($pass, $enchex) = @_; my(@encbin) = (); while (length($enchex) > 0) { push(@encbin, pack("h2", $enchex)); $enchex = substr($enchex, 2); } my($dec) = RC4($pass, join('', @encbin)); return $dec; }
5.暗号化は必要なのか?今回は,パスワードと実際にHTMLに記述するキーには暗号化復号化の関係を持たせたが, 必ずしもこういう方法をとる必要はない.例えば,生成したパスワードをサーバ上に保存しておいて, それに対してランダムなキーを発行してHTMLではそれを使う方法が考えられる. これならキーとパスワードの間に数学的な関連が無くなるので, 類推される可能性はゼロとなり安全だ. ただし,サーバ上に保存すると,溜まった古いデータを削除するタイミングを考える必要があるため, それはそれで面倒なところがある. 例えばうちのサイトのようにページ内容をサーバ側でキャッシュしている場合は,キーを発行するタイミングと実際にキーが使われるタイミングに大きなタイムラグが生じる可能性がある(数週間とかそれ以上)ので,発行からどれだけ時間が経過したら削除しても良いという判断が無理だという状況がある.
6.連投規制を併用する (2007.08.25 追加)なおこのように,投稿時に正しいパスワードの入力を必須としたが,パスワードの文字種と長さが少ないため,パスワードの強度はかなり低い.
そのため,総当たり攻撃で破られるという事態が結構現実的なのだ.(うちのサイトはやられました) これを防ぐためには,同一IPからの連続投稿をチェックして,パスワードを何回か連続で間違えた場合はそのIPからの投稿を一時的に停止するとか,考える必要がありそう. うちのサイトでは,同一IPからパスワードを2回連続で間違えた場合は,そのIPからの投稿を1分間禁止するという実装を行いました. |