セーフレベル 4 環境と Erubis, Tenjin

Ruby のセーフレベル4($SAFE=4)って、やっぱり、大事だと思います。果たしてセーフレベル 4 環境をどの程度信用して良いのかは確証が持てないところですが、とにかく丸腰で挑むよりはよっぽどマシだろう、という弱腰の姿勢で取り組んで行く次第です。

さて、そんなわけですから、セーフレベル4環境でErubisとTenjinを使ってみましょう。

Erubis

require "rubygems"
require "erubis"

thread = Thread.start do
  $SAFE = 4
  tmpl = Erubis::Eruby.new('<html><body><%= @content %></body></html>')
  env = Object.new.instance_eval do
    @content = "うぴょーん"
    binding
  end
  tmpl.result(env)
end

puts thread.value

この結果は次のようになります。

<html><body>うぴょーん</body></html>

というわけで成功です。Erubisさんはセーフレベル4環境でも実にうまく動作することが分かりました。楽勝!

Tenjin

さて問題は Tenjin です。まず、さっきの Erubis と似たような感じでやってみます。

require "rubygems"
require "tenjin"

thread = Thread.start do
  $SAFE=4
  context = Tenjin::Context.new(:content => "うぴょぴょーん")
  tenjin = Tenjin::Template.new
  tenjin.convert('<html><body>${@content}</body></html>')
  tenjin.render(context)
end

puts thread.value

この結果は次のようになります。

/usr/lib/ruby/gems/1.8/gems/tenjin-0.6.1/lib/tenjin.rb:245:in `instance_variable_set':
Insecure: can't intern tainted string (SecurityError)

この理由は Tenjin(0.6.1) の BaseContext#[]=が次のように定められているからです。

    244     def []=(key, val)
    245       instance_variable_set("@#{key}", val)
    246     end

しかし context 作成はどうせ無害のはずです。だからセーフレベル4環境の外に出すことにしましょう。

require "rubygems"
require "tenjin"

context = Tenjin::Context.new(:content => "うぴょぴょーん")

thread = Thread.start do
  $SAFE=4
  tenjin = Tenjin::Template.new
  tenjin.convert('<html><body>${@content}</body></html>')
  tenjin.render(context)
end

puts thread.value

これなら大丈夫?

/usr/lib/ruby/gems/1.8/gems/tenjin-0.6.1/lib/tenjin.rb:611:in `untaint':
Insecure operation `untaint' at level 4 (SecurityError)

というわけで今度は別のところでエラーが起こりました。この原因は Template#render 及び #_render にあります。

    609     ## create proc object
    610     def _render()   # :nodoc:
    611       return eval("proc { |_context| self._buf = _buf = #{init_buf_expr(
    611 )}; #{@script}; _buf.to_s }".untaint, nil, @filename || '(tenjin)')
    612     end
    620     ## evaluate converted ruby code and return it.
    621     ## argument '_context' should be a Hash object or Context object.
    622     def render(_context=Context.new)
    623       _context = Context.new(_context) if _context.is_a?(Hash)
    624       @proc ||= _render()
    625       return _context.instance_eval(&@proc)
    626     end

セーフレベル 3, 4 環境ではuntaintが認められません。このため #_render における 611 行目の untaint が問題となります。しかし考えてみれば #_render でやっていることは単純に @proc の作成だけです。別に内容がここで実行されるわけではありません。だからこれもやっぱりセーフレベル4の環境から出してやりましょう。ちょっと無理矢理感がありますが。

require "rubygems"
require "tenjin"

context = Tenjin::Context.new(:content => "うぴょぴょーん")

thread = Thread.start do
  $SAFE=2
  tenjin = Tenjin::Template.new
  tenjin.convert('<html><body>${@content}</body></html>')
  tenjin.instance_eval do
    @proc ||= _render
  end
  $SAFE=4
  tenjin.render(context)
end

puts thread.value

この方法は見た目が汚いので #_render を直接再定義した方が良いかも知れません。さて、これでうまくいったかな?

(tenjin):1:in `_buf=': Insecure: can't modify instance variable (SecurityError)

またエラーです。この原因は #_render の "proc { |_context| self._buf = _buf = ..." の部分で _context._buf= というのがあるからですね。これを回避するために先に taint しておきます。

require "rubygems"
require "tenjin"

context = Tenjin::Context.new(:content => "うぴょぴょーん")
context.taint

thread = Thread.start do
  $SAFE = 2
  tenjin = Tenjin::Template.new
  tenjin.convert('<html><body>${@content}</body></html>')
  tenjin.instance_eval do
    @proc ||= _render
  end
  $SAFE = 4
  tenjin.render(context)
end

puts thread.value

これで成功です。

<html><body>うぴょぴょーん</body></html>

というわけで、Tenjin でもセーフレベル4環境で動作させることが出来ました。Erubisは普通に動いたので、それと比べれば簡単とは言い難い感じですが、とりあえずこの程度ならば許容範囲内だと思います。

感想

信頼できないテンプレートが本気で心配な人は Liquid の方がいいと思います。LiquidはRubyのコードを直接書かないで済むのでより安心ではないでしょうか。でも文法覚えられないし、みんなLiquidって言っても知らないし、ね。Erubisが簡単でいいと思います。

あ、Tenjin のコード見てたら ArrayBufferTemplate っていうのがあって、これは一体どうなんでしょう。今回ErubisとTenjinの勉強になって良い経験でした。