タグ別アーカイブ: Perl

AkismetでPerl掲示板CGIのスパム対策を-実験編

スパムの巣窟と化した掲示板コレクション に掲載した掲示板は、このブログの最初の記事 掲示板スパムのツール投稿専用サーバーを見つけた で書いた約80万件のリストから、日本語の掲示板を一部(およそ200件以上)を抜き出し、現在もスパムに汚染され続けている掲示板(約100件あまり)をチョイスしたものです。

調査段階で特徴的だった事は、ISSOさんのスパム対策掲示板 (最近独自ドメインを取得されたようですネ)の約半数はクリーン、残り半数が汚染されていた点です。そして必ずしも新しいバージョンがスパムをブロック出来ているというわけではありませんでした。

これはあくまで推測ですが、NGワードファイル が最新かどうか、が分かれ目のように思います。コメント中に記載されたドメイン名などは所詮有限なワケで、ひたすらNGワードに登録し続けるというのも確かに有効な手段だと思います。ですが人の幸せをこころざし、対策版を一般公開したら最後、その裏を掻く投稿とのイタチごっこになってしまうのは、悲しいかな止むを得ない事です。

一方、最強の呼び声高い Akismet のメリットを挙げれば次のようになります。

  • スパム判定アルゴリズムが一般には非公開
  • たとえアルゴリズムが分かったとしても、その境界を見つけることは困難
  • たとえその境界が突破されても、ユーザーからの膨大なフィードバックで、より防衛精度が向上する

おそらく学習系のアルゴリズムを用いていると思いますが、ルール的な従来のスパム判定とアナログ的な学習系判定の概念的な違い(あくまで概念ネ 😀 )を図に表せば、次のようになるでしょう。
続きを読む

AkismetでPerl掲示板CGIのスパム対策を-準備編

I love SPAM™商標:©SPAM™ 素材:Wikimedia Commons

AkismetWordPress 専用かと思っていましたが、Protect Yourself from Parasite Spam with Akismet ≪ Weblog Tools CollectionMovable TypeDrupalPHP.NET など、20以上のアプリケーションシステムやプラットフォームでも使えることを知りました。その中には Perl 用のモジュールもあり、実際に掲示板 CGI に組み込むまでのステップを何回かに分けて解説してみたいと思います。
続きを読む

スパムに効く正規表現5-Shift_JISひらがな判定と文字数カウント

掲示板 CGI で英文スパムを弾くのに、ひらがなが含まれているかをチェックすることがあるでしょう。例えば、掲示板スパム対策 – 日本語がないメッセージの書き込みを禁止する では、コメントに Shift_JIS ひらがな、カタカナが含まれているかを判定するために、次の様な Perl の正規表が紹介されています。

if ($in{'comment'} !~ /(x82[x9F-xF2])|(x83[x40-x96])/) {
	&error("日本語を書いてください。");
}

このコードは例えば b、c という文字列を見逃してしまいます。何故ならこの文字列には x82 xa4 という、ひらがなの「う」を表すコードの並びが入っているからです。

    b      、       c
x82 x82  xa4  x82 x83

この場合は次のように、2文字連続するパターンを正規表現で表現すれば誤判定の頻度はグッと減るでしょう。

if ($in{'comment'} !~ /(x82[x9F-xF2])|(x83[x40-x96]){2}/) {
	&error("日本語を書いてください。");
}

しかし敵も然る者、一部に簡単な日本語を混ぜて投稿をしてきます。こうなると日本語の文字数と英数字の文字数を数え、一定以上日本語が含まれているかどうかをチェックしたくなります。今回はそんなエントリーです。

2010年10月1日追記

ロシア人の という意味のロシア語 Русское が2回続くパターンがあることが分かりました。

    Р         у         с         с         к         о         е
x84 x51  x84 x85  x84 x83  x84 x83  x84 x7b  x84 x80  x84 x75

ロシア語には сс を含む単語が結構あるので、上記正規表現の {2}{3} に変えるか、こういう対処療法がイヤな人は次章を参照してください。
続きを読む

スパムに効く正規表現4-Shift_JIS以外の文字を削除する

かなり前のことですが、掲示板 CGI (Shift_JIS で書かれた Perl スクリプト)のスパムフィルターにかかった投稿を管理画面で開いた際、レイアウトが崩れたことがありました。UTF-8 で投稿された記事をスクリプト側が処理できず、Shift_JIS で表示をしようとしたブラウザが タグの開始記号 < を解釈出来なかったのでは?、と当時その原因を推定しました。日本語環境から普通に投稿すれば、こちらの指定した charset で返してくるのがまっとうなブラウザの動作だと思うので、かなり怪しい投稿と言えます。

さて、タグが乗っ取られないように対策することにしたのですが、当時私の使っていた @nifty は一切の Perl 用モジュールが使えず、UTF-8 を処理できない jcode.pl を使用せざるを得ませんでした(Jcode.pm を個別にアップロードするという手はありましたが…)。そこで表題のような正規表現を考えたわけです。

現在では CGI が動作するホスティングサービスであれば encode モジュールが普通に使えるので、もはやニーズのないエントリーなのかもしれませんが、G アサーションを使った正規表現の応用と考えれば、誰かの役に立つかもしれないと思いポストします。
続きを読む

スパムに効く正規表現3-文字化けしないShift JIS空白の削除

掲示板 CGI には大抵、禁止ワードを含んだ投稿をスパム扱いする機能が備わっていますが、例えば「きんし」という単語に対し、敵は「き ん し」などとスペースを入れて投稿してきたりします。当然、CGI 側は次のような正規表現と s/// 演算子を使って記事中の空白・改行(x81x40 は Shift JIS で全角スペースを表します)を削除した後、投稿禁止ワードを検索したりするわけです。

$text =~ s/s|x81x40|<br>//ig;

一方 Perl は $text の中身が Shift JIS か UTF-8 かなどはお構い無しに、単にバイトデータ列としてマッチングと置換を行いますので、上記の Perl コードでは例えば a@Ab` に変わってしまうなど、文字化けが発生してしまいます。

勿論、人目に触れることのない処理なので影響はないのですが、今回は Shift JIS の文字列を正しく区切りながらマッチングを行う正規表現について考えてみたいと思います。
続きを読む

スパムに効く正規表現2-URLの分解とドメイン名の抽出

ウチのCGI掲示板には、スパムと判定された投稿からURLのドメイン名を抽出し、投稿禁止ワードとして一定期間保持する機能を持たせています。他のスパム判定フィルタと合わせることでモレを少なくする効果を狙ったものですが、特にスパム対策が未熟だった頃、この機能に引っかかったスパム投稿も少なくありませんでした。

さて、正規表現 URL 分解 を Google で検索する と第1位に「正規表現サンプル(URLを分解する)」が挙がり、次のような正規表現が紹介されています。

^(.+?)://(.+?):?(d+)?(/.*)?$

この正規表現は、左側のカッコから順に スキームホストポートパス へと分解してくれるのですが、http://127.0.0.1/ の様にIPアドレスで構成されたURLの場合、ホストが 127.0.0.、ポートが 1 となってしまうという問題点があります。

もちろんこれを改善する正規表現を作ることは出来るのですが、ここでは RFC2396日本語訳) に従ってURIを分解する正規表現を検証してみたいと思います。
続きを読む

スパムに効く正規表現1-aタグのURLを正しく自動リンクする

KENT WEByy-board などのCGI掲示板には大抵、書き込まれたURLを自動リンクする機能が備わっていますが、

<a href="http://example.com/">http://example.com/</a>

の様にaタグ付きで書き込まれた場合、次の様にURLの区切り位置を誤ってリンクが張られる事があります。

<a href="http://example.com/">http://example.com/</a>

今回は掲示板へのスパム書き込み自体を防止するネタではありませんが (タイトルがビミョー ;-))、このうっとおしい状況を改善する正規表現について書いてみます。
続きを読む

Perlのmap関数って、結構使える

前回 に引き続き、クッキー処理についてのネタを1つ。

HTMLやらPerlやらの勉強を始めて結構経ちますが、まだまだ初心者の域をちょっと超えたぐらいかなというBlogrammerにとって、使ったことの無い関数、パッケージ、オブジェクト指向プログラミングなど、勉強すべき項目は山ほどあり、そんななかの1つが map 関数でした。まずは元ネタの紹介から。

日本最強WEB講座 [Smart(スマートスマート)]配列の超絶テクニック大全集 で「ソートされていない配列からユニークな要素だけを取り出す」例が紹介されています。

@array = (1,3,3,2,4);
%tmp;
@sort = grep(!$tmp{$_}++, @array);
print @sort, "n";
> 1324

これがなぜmap関数に結びつくかというと、grep、mapは共に

grep 式, リスト
map 式, リスト

あるいは

grep {ブロック} リスト
map {ブロック} リスト

という使い方をするからです。即ち

リスト(配列)の要素1つ1つに対し、式あるいはブロックを評価・実行し、その結果をリストとして返す

という共通性を感じたからです。武道で言えば「型」みたいなものでしょうか。武道の型には基本の美しさを感じると思いますが、Perlにもこのような美しさを感じてしまうのは私だけでしょうか?

さてこれが分かってからは、foreachwhile で配列の要素1つ1つに何か処理をしている箇所をピックアップし、map 関数が適用できないか洗い上げました。ココでは掲示板CGIの YY-BOARD に実装されているクッキー処理を例を挙げておきます。まずは init.cgi 中に定義されたオリジナルのコードです。

#-------------------------------------------------
#  クッキー取得
#-------------------------------------------------
sub get_cookie {
	local($key, $val, *cook);

	# クッキー取得
	$cook = $ENV{'HTTP_COOKIE'};

	# 該当IDを取り出す
	foreach ( split(/;/, $cook) ) {
		($key, $val) = split(/=/);
		$key =~ s/s//g;
		$cook{$key} = $val;
	}

	# データをURLデコードして復元
	@cook=();
	foreach ( split(/<>/, $cook{'YY_BOARD'}) ) {
		s/%([0-9A-Fa-f][0-9A-Fa-f])/pack("H2", $1)/eg;

		push(@cook,$_);
	}
	return @cook;
}

で、map を使ってこんな風に書き直して使っています。

#-------------------------------------------------
#  クッキー取得
#-------------------------------------------------
sub get_cookie {
	# クッキーを分解取得する
	my %cook = map { my ($key, $val) = split /=/; $key => $val; } (split /;s*/, $ENV{'HTTP_COOKIE'});

	# データをURLデコードして復元
	map { s/%([0-9A-Fa-f][0-9A-Fa-f])/pack("H2", $1)/eg; $_; } (split /<>/, $cook{'YY_BOARD'});
}

2つの foreach をそれぞれ1行に凝縮し、コードの肝心な部分はたった2行で済んでしまいます。
う~ん、こんなことに美しいを感じてしまうのは変でしょうか?

スパム投稿にHTTPステータスコード"404 Not Found"を返す

前の記事で掲示板スパム対策サイトで

  1. スパマーに掲示板URLを知られることを防ぐ
  2. スパマーにスパム投稿させないようにする
  3. 投稿されたスパムをフィルタリングする

が紹介されていることを書きました。1. はさらに

  • URLを隠す
  • URLが検索サイトにインデックスされるのを防ぐ
  • 検索サイトにインデックスされたURLの削除を依頼する

などがあり、JavascriptによるURLの隠蔽やメタタグ・robots.txtを使った検索サイトへのインデックス防止策が紹介されています。

さて、ここではスパマーに掲示板のURLを収集されてしまった後のささやかな抵抗として、PerlからHTTPステータスコードを返す試みについて記述しておきます。ただし現時点で効果のほどが確認できたわけではないのですが…

その前に、例のスパム投稿専用(と思われる)サーバーで見つけたログの確認です。

投稿先が見つからない?

投稿先が見つからない?

見つけたフォームのactionで指定されたページにスパムコメントを送信したけれど「404 Not Found」が返ってきた、という状態だと思います。掲示板CGIプログラムでよく使われているPerlなどでこの状態を再現しスパムを撃退する、という考えです。まずは実際の応答の作り方を知るために、存在しないページにアクセスした場合の様子を、FirefoxのアドオンLive HTTP Headersで観測します。

HTTP/1.x 404 Not found
Server: ZWS
Date: Thu, 19 Nov 2009 15:07:09 GMT
Connection: close
Content-Type: text/html

上記は@niftyにアクセスしたときの様子です。Perlであれば「Server:」の右辺は環境変数「$ENV{‘SERVER_SOFTWARE’}」で得られますし、「Date:」も何とかなりそうです。が、Studying HTTP に詳しく解説されているように、レスポンスには「HTTP Header Field」と「CGI Header Field」があり、CGIプログラムは後者に則る必要がありそうです。理解を助けるためにHTTP~CGIの仕組みと仕様の概念図を描いてみました。

HTTPサーバーと規格の概念図

HTTPサーバー~CGIの概念図

この図はHTTPサーバーとCGIに特化していますが、外部のプログラムやサービスと(CGIではない)別なインターフェースを持つサーバー(MicrosoftのASPなど)も当然考えられますね。

で、CGIからは「CGI Header Field」+ドキュメントの中身である「Message Body」がレスポンスとして返せることになります。「CGI Header Field」は主に

  • Content-Type
  • Location
  • Status

が定義されていて、「Status」としてはHTTPのステータスコードと書式が使えるようです。

日本最強WEB講座 [Smart(スマートスマート)] には「完全な HTTP メッセージを出力するプログラム」として以下が紹介されています。

#!/usr/local/bin/perl

# 完全なレスポンスヘッダの出力
print "HTTP/1.1 200 OKn";
print "Date: Wed, 03 Jan 2001 17:22:53 GMTn";
print "Server: Apache/1.3.9 (Unix)n";
print "Connection: closen";
print "Content-Type: text/html", "nn";

# HTMLの出力
...

確かにこのPerlコードは@niftyやLaCoocanで動作します。しかしサーバーからの応答は次のようになり、「Content-Type:」以外は指定したようにはなっていません。

HTTP/1.x 200 OK
Server: ZWS
Date: Mon, 23 Nov 2009 15:05:04 GMT
Transfer-Encoding: chunked
Content-Type: text/html

また手元にあるWindows版Apache2.2.11で試してみると、「print “HTTP/1.1 200 OKn”;」の部分で「malformed header from script」として「Internal Server Error」が起きます。サーバーによって仕様が異なるのは、HTML研究室ヘッダの書き方で解説されているようにCGI 1.1が定義されるまでの経緯が関係しているのでしょうか。

結局「404 Not Found」を返すPerlコードはシンプルに以下で良いということが分かりました。

#!/usr/local/bin/perl
print "Status: 404 Not Foundn";
print "Content-Type: text/htmlnn";
...

まぁいずれにしても、これらを使って明らかにスパムと分かるアクセスがあったら、ステータスコードを返して撃退できるかもしれません。

って、お前は試してないのかって? そもそもフォームを隠蔽しているのでスパム投稿自体が無いのと、借りている LaCoocanのサーバーはIPアドレスの逆引きが成功する時と失敗する時があるので、「明らかにスパム」の判別ができないのです。 😉