/ / 最新

swk's log - 計算問題によるコメントスパム対策 - 固定パラメータ攻撃対策

2007-05-05

* 計算問題によるコメントスパム対策 - 固定パラメータ攻撃対策 [logging] 1 user

というわけで [2007-04-30-1],まず世の中の CAPTCHA とかではどうやっているかというと,どうやらワンタイムトークンとするのが普通らしい.

今回のケースに置き換えると,サーバは計算問題を生成して (これがトークン),それをサーバ側に保存しておき,ユーザから送られて来た答えと,その保存しておいた問題 (の正解) と照合する.

ということでそれを実装すればよいのだけど,どうもぐっと来ない.なぜぐっと来ないのか考えた結果,思い当たったのは以下の 2 点:

  • くっつきBBSの場合,コメント閲覧ページとコメント投稿フォームが同一なので,コメントを閲覧するたびにトークンを保存しなくてはならなくなって何か嫌だ
  • 上と関連するけど,コメント投稿フォームをリクエストするときの method は GET なので,副作用を起こすのが気持ち悪い

ぐっと来ないものを作ってもろくなことにならないのでとっとと却下する.


というわけで考え方を変えてみる.要は,投稿のリクエストに関して,

  • サーバ側で,正しいパラメータの組合せの判断が可能で,
  • 攻撃者側では,正しいパラメータの組合せを不正に生成するのが事実上不可能で,
  • 一度成功したパラメータは再利用できない

という条件を満たせばよいのかな.これでどうだろう.

  • サーバ側で固定の秘密パスフレーズ $p を用意しておく.
  • 足し算の問題を生成するとともに,適当なトークン $t も生成する.
  • 足し算の答え $a と $p と $t からハッシュ値 $h = hash($a, $p, $t) を生成して,$h と $t を hidden input としてクライアントに渡す.
  • ユーザは投稿時,足し算の答え $a' とともに (hidden input の) $h と $t をそのままサーバに送り返す.
  • サーバは,まず $t が過去に投稿受理した際のものと同一でないかを確認する.OK であれば,hash($a', $p, $t) と $h を比較して,一致したら投稿を受理する.受理した際は,$t を保存しておく.

実装はとりあえず以下のような感じ.$page_template_default には

<div class="arith">
<br>
コメントスパム回避のため,以下の足し算の答えを半角でご記入下さい: <br>
$arith_x + $arith_y = <input class="field" name="arith" value="">
<input type="hidden" name="arith_token" value="$arith_token">
<input type="hidden" name="arith_hash" value="$arith_hash">
</div>

を仕込み,これを表示する直前に

my $arith_x = int(rand(9)) + 1;
my $arith_y = int(rand(9)) + 1;
my $accepted_tokens = {}; 
if (-e $arith_token_file) {
    $accepted_tokens = Storable::lock_retrieve($arith_token_file);
}
my $arith_token; 
do {
    $arith_token = Digest::MD5::md5_hex($logid .
      				  $q->remote_host() .
      				  int(rand(2 ** 32)));
} while (defined($accepted_tokens->{$arith_token}));
my $arith_hash = Digest::MD5::md5_hex($arith_token .
      				$arith_passph .
      				($arith_x + $arith_y));

とする.ここでは token ファイルは読み込んでいるだけなのに注意. if ($mode eq "write") { のときの処理は,

  my $arith = $q->param('arith') + 0;
  my $arith_token = $q->param('arith_token');
  my $arith_hash = $q->param('arith_hash');
  my $accepted_tokens = {}; 
  if (-e $arith_token_file) {
      $accepted_tokens = Storable::lock_retrieve($arith_token_file);
  }
  if (defined($accepted_tokens->{$arith_token})) {
      exit;
  } elsif ($arith_hash ne Digest::MD5::md5_hex($arith_token .
                                               $arith_passph .
                                               $arith)) {
      exit;   				 
  } else {
      $accepted_tokens->{$arith_token} = 1;
      Storable::lock_store($accepted_tokens, $arith_token_file);
  }

とした.

トークンの生成は適当で構わないのだけど,発行済みのトークンで過去に受理されたもの,および受理される可能性のあるものと重複しないようにだけは気をつける必要があるので,何かむにゃむにゃな処理をやっている.

うーむ,ちっともお手軽じゃなくなってきた.いろいろ工夫してみたところで所詮足し算を実行されたら終わりですからね.なんかベクトルが間違っている気がしないでもない.

現状の問題点:

  • 受理したトークンの記録が単調増加.expire する仕組みが必要.
  • 投稿を受け付けるする際の,$arith_token_file の read から write までの一連の処理が atomic でない.だから,場合によっては書き込んだはずの受理済トークンが失われて,二度目の使用を許してしまう場合があり得る.
  • MD5 ってもうダメなんでしたっけ?

というか何か根本的に見過ごしていることがあるような気がしてならない….どんなもんでしょ.


(追記)

2007年05月06日 kazuhooku しょうもないつっこみしてすみません。よっぱらいつつの感想: syncookies のようなゼロ記憶だと難しいのかな。

http://b.hatena.ne.jp/entry/http://www.kagami.org/diary/2007-05-05-1.html

恥ずかしながら syncookies って初耳だったのでちょっと勉強.

なるほど.トークンを乱数を使って作る代わりに時刻を使うことにして,一定時間経過してたら reject するってことになりますかね.ハッシュ値の計算に時刻をつっこんでおくことで,サーバ側の保存無しで expire をチェックできるところがミソだと理解しました.

「一定時間」をうまく設定してあげる必要があるかも.短いとコメントを書いているうちに expire しちゃうし,長いとその間は固定パラメータ攻撃されちゃいますよね.まあ 1 時間くらいにしておけば実用上十分な気がしますが.

…というのが SYN cookies の動作を単純にマッピングした場合の話だと思いますけど,もしかするともっとうまい応用のしかたがあったり…?

関連記事:
[2007-06-28-1] コメントスパムがやって来た
[2007-04-30-1] 計算問題によるコメントスパム対策の実装

最終更新時間: 2009-01-04 15:31


Shingo W. Kagami - swk(at)kagami.org