Rubyの標準添付XMLパーザライブラリ"REXML"を使う上での注意点やTipsなどを順次まとめていきます。
Last-Modified: 2008/09/02 18:54
Table of Contents
REXMLのDoS脆弱性1)
Ruby 1.8.6-p287以前, 1.8.7-p72以前, 1.9 (2008/8/23)以前のすべてのバージョンに添付のREXMLには,XML entity explosion attackと呼ばれる攻撃手法により,ユーザから与えられたXMLを解析するようなアプリケーションをサービス不能(DoS)状態にすることができる脆弱性が存在する。
http://www.ruby-lang.org/security/20080823rexml/rexml-expansion-fix.rbより,モンキーパッチ (実行時にライブラリを修正するパッチ)をダウンロードし,REXMLより前に"rexml-expansion-fix"がロードされるようにアプリケーションを修正することで対処可能。
require "rexml-expansion-fix" # 追加 require "rexml/document"
mod_rubyを用いている場合には,アプリケーションの修正後,忘れずにapacheをreloadすること。
はまりやすい点
dupやcloneでは子要素がコピーされない
REXML::Element#dupやcloneは子要素をコピーしない。
子要素を含めてコピーしたいときはParent#deep_cloneを使用する。
el=REXML::Element.new('el')
root=REXML::Element.new('root')
root.push(el)
root.to_s #=> "<root><el/></root>"
root.dup.to_s #=> "<root/>"
root.deep_clone.to_s
#=> "<root><el/></root>"
ただしTextクラスなど子要素を持たないクラスではdeep_cloneメソッドが定義されていない。
あるElementオブジェクトの子要素になっているノードを他のElementオブジェクトにpushすると元の親要素から子要素ノードが削除される
el=REXML::Element.new('el')
parent1=REXML::Element.new('parent1')
parent1.push(el)
parent1.to_s #=> "<parent1><el/></parent1>"
parent2=REXML::Element.new('parent2')
parent2.push(el)
parent2.to_s #=> "<parent2><el/></parent2>"
parent1.to_s #=> "<parent1/>"
配列の感覚で扱うとはまる。
元の親要素から削除したく無い場合は,「dupやcloneでは子要素がコピーされない」の項の方法を用いてノードのコピーを作成する。
parent1.to_s #=> "<parent1><el/></parent1>"
parent2=REXML::Element.new('parent2')
parent2.push(el.deep_clone)
parent2.to_s #=> "<parent2><el/></parent2>"
parent1.to_s #=> "<parent1><el/></parent1>"
Comment#to_sがコメントにならない
Comment#to_sはComment#stringのaliasになっている2)。Comment#stringはコメント文字列を出力するため,to_sメソッドもコメント文字列がそのまま出力されてしまう。
c=REXML::Comment.new("hoge")
c.to_s #=> "hoge"
to_sメソッドのかわりにComment#writeメソッドを使用する。
c=REXML::Comment.new("hoge")
str=''
c.write(str)
str #=> "<!--hoge-->"
Element, Instruction, CDataと挙動をあわすには,以下のようにComment#to_sを再定義すればよい。
class REXML::Comment
def to_s(indent=-1)
rv=''
write(rv,indent)
rv
end
end
c=REXML::Comment.new("hoge")
c.to_s #=> "<!--hoge-->"
Tips
空要素の終了タグを強制的に出力する
文書をxhtmlとして出力する際のscriptタグなど,空要素であっても終了タグがないと困る場合には,終了タグを出力したいElementに空のTextオブジェクトを追加する。
el=REXML::Element.new('script')
el.to_s # => "<script/>"
el.add_text('')
el.to_s # => "<script></script>"
ただしhas_text?の結果はtrueとなるので注意が必要。
出力時に内容を書き換える
たとえば以下のようなXML文書で<last-modified />を出力時のタイムスタンプに置き換えたいとする。
<root> <title>ほげ</title> <last-modified /> <p>ほげほげほげ…</p> </root>
REXMLではto_sメソッドもwriteメソッドが呼び出される3)ので,Element#writeを以下のように再定義するとよい。
class REXML::Element
alias _write write unless self.respond_to?(:_write)
def write(output=$stdout,indent=-1,transitive=false,ie_hack=false)
case self.name
when 'last-modified'
output<<Time.now.to_s
else
_write(output,indent,transitive,ie_hack)
end
end
end
Instruction(<?...?>), CData(<![CDATA[...]]>)についても同様。
たとえばInstruction#writeを以下のように再定義すれば,<?ruby スクリプト?>の形式で,eRubyのようにXML中にrubyのスクリプトを埋め込むことが可能となる4)。
class REXML::Instruction
alias _write write unless self.respond_to?(:_write)
def write(output=$stdout,indent=-1,transitive=false,ie_hack=false)
case self.target
when 'ruby'
output<<eval(self.content)
else
_write(output,indent,transitive,ie_hack)
end
end
end
xml=REXML::Document.new('<random><?ruby rand.to_s ?></random>').root
xml.to_s #=> "<random>0.359555345264576</random>"
xml.to_s #=> "<random>0.0277818386758441</random>"
xml.to_s #=> "<random>0.875877708416071</random>"
Comment(<!--...-->)は,「Comment#to_sがコメントにならない」で述べたように,to_sメソッドがstringのaliasになっているため,この方法ではうまくいかない。
直接,Comment#to_sを再定義するのが楽。
1) 一次情報: REXMLのDoS脆弱性 (www.ruby-lang.org)
2) rexml/comment.rb
3) rexml/node.rb
4) ただしこの実装はbindingを指定できないので実用的でない。
