Rubyの標準添付XMLパーザライブラリ"REXML"を使う上での注意点やTipsなどを順次まとめていきます。

Last-Modified: 2008/09/02 18:54

RubyXMLTips

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を指定できないので実用的でない。