/ 最近 .rdf 追記 編集 設定 本棚

脳log[20100308] spam絨毯爆撃対策



2010年03月08日 (月) obsoleteって形容詞だと思うんだけど推測辞書に obsoletedや obsoletingが含まれてるんだよね(辞書には載っていないが)。obsoleted - Wiktionaryによれば nonstandardだけど一部の分野で使われている、ということらしい。

最終更新: 2014-12-05T17:23+0900

[tDiary] spam絨毯爆撃対策

送信元が限られてるのかワンパターンだったので、これまでは </a> を NGワードにしてほとんどの spamを防いでいたんだけど、今日は手ひどくやられた。

フィルタはインストール方法がよくわからない。Comment-key Filterを tdiary/filter/ に設置したがエラーが出るので、とりあえず最新の 2.3.3.20091124にアップデートした。そうすると plugin/60sf.rbが有効になっているはずなので misc/filter/ にフィルタスクリプトを、misc/filter/plugin/ にフィルタに関連する設定などを表示するプラグインスクリプトを、misc/filter/plugin/ja/ などにプラグインの言語リソースをインストールし、設定画面でフィルタを有効にするといずれのスクリプトも読み込まれるようになる。

 スパムフィルタ1: Comment-key Filter & Plugin version 0.5.0

コメントフォームにキー文字列を埋め込み、コメントが HTMLフォームを通して投稿されたことを確認する。キーは日記設置者の決めた文字列と日記の日付によって一意に決められるので、ある日のキー文字列を一度取得すればその日には何件でも機械的に投稿することができる。GETして HTMLを切り刻む手間をかければ最初から機械的に投稿することもできる。そんなのは防げない。

tDiary-2.3.3.20091124にインストールするのなら key.rbを misc/filter/key.rbへ、comment_key.rbを misc/filter/plugin/key.rbへ、ja/comment_key.rbを misc/filter/plugin/ja/key.rbへコピーすれば良い。key.rbというファイル名と KeyFilterというクラス名は対応しているので、key.rbから comment_key.rbへの改名はやめたほうがいい。コピー後にフィルタを有効にし、キー文字列を設定する。

 スパムフィルタ2: limit_freq.rb

連投対策。同じ IPアドレスからの連続する投稿をはじく。30分につき 3件までしか許可しない、など。

# coding: utf-8
#
# limit_freq.rb: 
#
#  a spam-filtering plugin of tDiary
#  rejects frequent comments posted from an IP address.

module TDiary::Filter
	class LimitFreqFilter < Filter

		def comment_filter( diary, comment )
			now = Time.now.to_i
			comment_i = Comment_t.new( now, @cgi.remote_addr )

			# 数値形式の日時と文字列形式の IPアドレスの二要素
			# 配列の配列を、日時をもとに昇順にソートしたもの。
			# log = [[123456, "1.2.3.4"], [234567, "5.6.7.8"],...]
			log = []

			require 'pstore'
			ps = PStore.new( cache_path )
			ps.transaction( false ) { |db|
				log = db.fetch( DBRoot, log )

				# 新しいコメントを logに追加する。
				log.insert( ArrayExtension.lower_bound(log){|pair| pair.first - comment_i.time }, [comment_i.time, comment_i.ipaddr] )

				# 古い logを捨てる。
				oldest = now - time_span
				log.slice!( 0 ... ArrayExtension.lower_bound(log){|pair| pair.first - oldest } )

				db[DBRoot] = log
				db.commit
			}

			# 残った logに投稿者の IPアドレスがいくつ含まれるか数え、
			# その数が閾値を超えていないか確かめる。
			count = 0
			if log.all?{|pair|
				count += 1 if pair.last == comment_i.ipaddr
				count < threshold
			} then
				return this_is_not_a_spam( comment )
			else
				debug("limit_freq.rb: spam: the # of comments posted from #{comment_i.ipaddr} exceeds threshold:#{threshold} within the last #{time_span.to_i}seconds.")
				return this_is_a_spam( comment )
			end
		rescue
			debug("limit_freq.rb: BUG: #{$!}")
			return this_is_not_a_spam( comment )
		end

	private
		DBRoot = 'LimitFreqLog'
		Comment_t = Struct.new(:time, :ipaddr)

		def cache_path
			return File.join( (@conf.cache_path || "#{@conf.data_path}cache"), 'limit_freq.data' )
		end

		# どれだけの期間(秒単位)、コメントの投稿頻度の判定に
		# 利用するログ(日時とIPアドレスのペア)を保存するか。 
		def time_span
			30 * 60
		end

		# time_span当たり、何件目の投稿からを拒否するか。
		def threshold
			4
		end

		module ArrayExtension
			# ソート済みだという前提を活かしていない!
			def lower_bound( arr, &block )
				index = 0
				arr.length.times{
					return index if 0 <= yield( arr[index] )
					index += 1
				}
				return index
			end
			module_function :lower_bound
		end
	end
end

やめればいいのに本体もちょろちょろと変更。

  • 設定画面における [セキュリティ]>[spamフィルタ]>「spamの扱い spamと判定されたツッコミを{非表示にする|捨てる}」という項目は tdiary/filter/spam.rbに固有の設定らしいのだが、そんなこととは夢にも思わないので、最初は自作のフィルタでスパム判定したコメントが捨てられることに困惑した。設定を本体に取り込んで他のフィルタからも利用しやすくした。
  • limit_freq.rbや limit-freq.rbや limit_--_freq.rb というフィルタに対しては LimitFreqFilterという名前のクラスを要求するようにした。(これらのファイルが複数存在するときは困るなあ。読み込み順で最終定義が変わりそう)
  • どういうわけか無名クラスの下に定義された TDiary::Filter定数を参照してエラーになるので ::TDiary::Filterとした。
Index: core/tdiary.rb
===================================================================
--- core/tdiary.rb	(リビジョン 44436)
+++ core/tdiary.rb	(作業コピー)
@@ -342,6 +342,22 @@
 
 				@logger.info("#{@cgi.remote_addr}->#{(@cgi.params['date'][0] || 'no date').dump}: #{msg}")
 			end
+
+		private
+			# config: hide or drop spam comment
+			def hide_spam?
+				return @conf.options.include?('spamfilter.filter_mode') && @conf.options['spamfilter.filter_mode']
+			end
+
+			def this_is_a_spam( comment )
+				comment.show = false
+				return hide_spam?
+			rescue
+				return false # comment could be a String(@cgi.referer).
+			end
+			def this_is_not_a_spam( comment )
+				return true
+			end
 		end
 	end
 
@@ -1303,7 +1319,7 @@
 			filter_path = @conf.filter_path || "#{PATH}/tdiary/filter"
 			Dir::glob( "#{filter_path}/*.rb" ).sort.each do |file|
 				require file.untaint
-				@filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).capitalize}Filter" )::new( @cgi, @conf, @logger )
+				@filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter" )::new( @cgi, @conf, @logger )
 			end
 		end
 
Index: core/plugin/60sf.rb
===================================================================
--- core/plugin/60sf.rb	(リビジョン 44436)
+++ core/plugin/60sf.rb	(作業コピー)
@@ -4,10 +4,9 @@
 # Modified by KURODA Hiraku.
 
 SF_PREFIX = 'sf'
-@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a
-@sf_path = @sf_path.collect do |path|
-	/\/$/ =~ path ? path.chop : path
-end
+@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a.map{|path|
+	path.chomp('/')
+}
 
 # get plugin option
 def sf_option( key )
@@ -128,7 +127,7 @@
 			if File.readable?( path ) then
 				begin
 					require path
-					@sf_filters << TDiary::Filter::const_get("#{File::basename(filename, ".rb").capitalize}Filter")::new(@cgi, @conf, @logger)
+					@sf_filters << ::TDiary::Filter::const_get("#{File::basename(filename, ".rb").split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter")::new(@cgi, @conf, @logger)
 					plugin_path = "#{dir}/plugin/#{filename}"
 					load_plugin(plugin_path) if File.readable?(plugin_path)
 				rescue Exception

 @2010-03-09: IPアドレスブロック

一件二件すり抜けるぐらいはいいかと思っていたがここ数日は毎日なのでいいかげん腹が立つ。なによりアクセスログを見たらヒット数、転送量トップが spammerだというのが決定的に許せない。節度を知れ。iptablesはいじれないので .htaccessで Apacheに拒否してもらう。


 @2010-03-11: 対応が早いね

spamコメントがくる月っていうのは、いつもの決まった 2、3か国からのアクセスではなく 10近い国から少数ずつアクセスがあるもんだけど、日単位では固定の IPアドレスから数十件の spamがくるのが常だった。でも今日は一件一件みごとに IPアドレスが異なっている。IPアドレスブロックで次のステージに進んでしまったのか。

spamコメントの目的が特定の URLへの誘導であるかぎりは、コメントに含まれる URLのドメインが白か黒かを判定する方法が有効でしょうね。外部に問い合わせるのは避けたかったんだけど、spamコメントの内容が URLも含めてワンパターンだから実際の問い合わせはごくごく限られた回数にできそうだし、悪くないかな。

 CodeRepos

ここで上記の comment-keyフィルタも含めてスパムフィルタの最新版が管理されていた。 >http://coderepos.org/share/browser/platform/tdiary/filter


 こぼれ話: rel="true"

plugin/00default.rbに含まれるメソッド navi_itemを自分でも使っていたのだけど、tDiaryをアップデートしたら三番目の引数が真偽値からリンクの rel属性文字列へと変更されているせいで rel="true" なるリンクができていた。こうする。

def navi_item( link, label, rel = nil )
	rel = "nofollow" if rel == true # backward compatibility

 @2010-03-09: 日記の編集もキャッシュベース?

スパムコメント一掃のために YYYYMM.tdcを削除したけど日記の編集画面からコメントが消えない。YYYYMM.parserを開くとコメントが含まれていたし、これを削除すると編集画面からもコメントが消えた。原因はデータファイルを直接削除するというイレギュラーな操作だけど、日記の編集はマスターデータに対して行いたい気もする。