Rubyのセーフレベル4環境とその使い方

昨日、Ruby 1.8.5-p231, 1.8.6-p230, 1.8.7-p22, 1.9.0-2 がリリースされましたね - ¬¬日常日記 に書いた通り、Rubyのセーフレベル4環境に関する情報は案外少ないのですが、ぐだぐだ言っても仕方ないので自分でまとめておくことにします。記憶力がほとんどない自分はまとめておかないとすぐに忘れちゃうからです。本当はまとめても忘れちゃうのですけれども、後で自分で読み直すために書いておきます。

セーフレベル4はセーフレベル2とどう違うの?

Rubyのセーフレベルは0から4まであります(昔はもっとあった?いつだったかセーフレベル5という記述を見たような気がするのですが)。ただし実際にセキュリティーモデルとして使用する観点から言えば、重要なのはセーフレベル2(セーフレベル1もほぼ同様ですが、2の方が制限が強くより安全なので、私にとっては2が主流なのです)と4の二つが重要です。

セーフレベル2環境は信用できない入力からプログラムを守るためのもので、例えばCGIなどでユーザからの入力に危ない文字列が含まれている可能性に対処する場合に用いられます。つまり、信用できるプログラムを実行している時に信用できない入力から身を守るものです。Perlのtaintモードと似たようなもの(他の言語でも同様の機構が用意されている場合が多いのではないかと思います)ですから、これには馴染みのある方が多いと思います。

一方セーフレベル4環境は、そもそも信用できないプログラムを実行するためのものです。信用できないプログラムを実行する場合とは、例えばウェブブラウザがJavaScriptで書かれたスクリプト(誰が書いたか分からない場合がほとんどで、また悪意をもって書かれているかも知れません)を実行するような場合を考えれば良いと思います。JavaJavaScriptではサンドボックスという限定的な環境においてプログラムを実行させることで危険な操作を制限するそうですが、これと同様のことをRubyではセーフレベル4において実現することが出来ます。

どんな時に使うの?

RubyJavaアプレットJavaScriptと違ってブラウザの中で動かすわけじゃないんだから、そんなの本当に必要あるの?と思うかも知れません。確かにセーフレベル4環境を使わなければならない必然性が存在する場面というはほとんどないのではないかと思います(信用できないプログラムを実行しようと思うのが間違いだ!と考える人がいても不思議ではありませんし、セキュリティーのことだけを考慮すれば多分そうなんだろうと思います)。しかしこれを使えば簡単に安全性と利便性の折り合いをつけることが出来ます。

例:危険な URI::HTTP#require

例えばウェブ上にあるRubyスクリプトをrequireしたいと思って次のように URI::HTTP を拡張したとします。

require "open-uri"

class URI::HTTP
  def require
    eval(read, TOPLEVEL_BINDING)
  end
end

さて次のような内容のスクリプトhttp://file.dynamic-semantics.com/hatena/20080622-1.rb)があるとします。

class Foo
  def bar
    "Hello, World!"
  end
end

これを先程の拡張した URI::HTTP#require で呼び出してみます。

URI("http://file.dynamic-semantics.com/hatena/20080622-1.rb").require

puts Foo.new.bar #=> "Hello, World!"

というわけでウェブ上にあるRubyスクリプトをrequireすることが出来ました。ただし当然これは危険極まりない行為です。例えば次のような内容のスクリプトhttp://file.dynamic-semantics.com/hatena/20080622-2.rb)を require してしまったら勝手にファイル hello.txt を消去されてしまいます。

File.delete("hello.txt")

class Foo
  def bar
    "Hello, World!"
  end
end

同様にこうしたスクリプトにおいては好きにファイルを作成したり消去したり出来るわけです。require する前にちゃんと自分で内容を確認すればいいんじゃない?と思うかも知れませんが、いつスクリプトの内容が差し替えられてしまうかも分かりません。従って URI::HTTP#require は危険な試みであることになります。でも、便利そうなのに...と諦められない人も居るかも知れません。

しかしこんな時こそ、セーフレベル4環境の出番です!

セーフレベル4環境の利用方法

セーフレベル4環境においては「危険な操作」を実行することが出来ません(「危険な操作」の具体的な内容は後で説明します)。例えば先程の例にあったようなファイルの削除は「危険な操作」のひとつです。セーフレベル4環境を利用するのはとても簡単で、"$SAFE=4"を設定するだけです。

$SAFE = 4
File.delete "hello.txt"
#=> Insecure operation `delete' at level 4 (SecurityError)

というわけで、例外 SecurityError が発生し、ファイルの削除に失敗します。これなら URI::HTTP#require の例において見たような問題は解決しそうです。もちろん一度セーフレベルを4に設定しておけば、これを変更することは出来ません。

$SAFE = 4
$SAFE = 0
#=> Insecure: can't change global variable value (SecurityError)
スレッドの活用

ただしこのことから明らかであるように、一度セーフレベルを4に上げてしまったら二度とファイル操作だけでなく諸々の操作が出来ないわけですから、これはこれで不便です。今自分が書いているプログラムは明らかに自分にとって信頼できるのだから、ファイル操作とか文字列の出力とか、したいですよね。そんな時にはThreadを用いてセーフレベル4環境の範囲を限定します。

$SAFE = 0

Thread.new do
  $SAFE = 4
  # このスレッドにおいて信頼できないプログラムを実行
end

# メインスレッドのセーフレベルは0のままなので制限がありません
puts $SAFE #=> 0

これはそれぞれのスレッドが独立してセーフレベルを保持していることを利用しています。このように信頼できないプログラムだけをスレッドによって括り出すことにより、セーフレベル4を環境を非常に手軽に利用できます。

Proc オブジェクトの活用

また一方でセーフレベル4環境においても一切ファイル操作が出来ないと不便ですよね。全ての操作を許せば問題があるとしても、その一部を限定的に許可しておいた方が実用的です。こうした場合には、Procオブジェクトを利用して次のようにします。

# Proc オブジェクトは作成された時点でのセーフレベルを保持
$save = Proc.new do |content|
  # "test.txt" というファイルは作成して良い
  File.open("test.txt", "w") do |file|
    # ただし 10 文字だけ
    file.write content[0...10]
  end
end

Thread.new do
  $SAFE = 4
  # 以下は信頼できないコード
  # ここで $save を呼び出すとファイルの書き込みは $save の持つセーフレベルで実行されるため
  # 例外 SecurityError が起きません
  $save.call("a"*1000)
end

これを実行すると"test.txt"というファイルが作成され、またその内容は"aaaaaaaaaa"になります。これはProcオブジェクトは作成された時点でのセーフレベルを保持していることを利用しています。このように許可したい操作をProcオブジェクトとして作成しておき、それをセーフレベル4環境から呼び出す、という形でセキュリティモデルの極めて柔軟な運用が可能です。

逆に言えば、Procオブジェクトを迂闊にセーフレベル4環境から参照可能にしないように注意して下さい。意図せず呼び出されてしまう可能性があります。ちょっと極端なのであまり実際的ではないと思いますが、例えば以下のようなコードはセーフレベル4環境から"test.txt"を削除する事が可能です。

class Foo
  def initialize
    @proc = Proc.new {File.delete "test.txt"}
  end
end

foo = Foo.new

$SAFE = 4
foo.instance_eval do @proc.call end

あと当然ですけどProcオブジェクトの中で迂闊なことをやったらそもそも実も蓋もないことになってしまうので気をつけて下さい。

セーフレベル4環境における「危険な操作」とは何か

では「危険な操作」が具体的に何であるのかの説明をしておきます。セーフレベル2環境までの制限に加えてセーフレベル4環境では以下のような操作(マニュアルからの抜粋です)が「危険な操作」として禁止されます。これらがなぜ禁止されるべきなのかを考えていくと楽しいかと思います。

  • Object#untaint
  • Object#taint
  • トップレベルの定義の変更(autoload, load, include)
  • 既存のメソッドの再定義
  • Objectクラスの定義の変更
  • 汚染されていないクラスやモジュールの定義の変更 およびクラス変数の変更
  • 汚染されていないオブジェクトの状態の変更
  • グローバル変数の変更
  • 汚染されていないIOやFileを使用する処理
  • IOへの出力
  • プログラムの終了(exit, abort) (なおout of memoryでもfatalにならない)
  • 他のスレッドに影響が出るThreadクラスの操作 および他のスレッドのThread#[]
  • ObjectSpace._id2ref
  • ObjectSpace.each_object ruby 1.7 feature
  • 環境変数の変更
  • srand

ただし eval は「危険な操作」ではありません。この理由は「危険な操作」はeval内で実行できないために問題がないからです。そもそも信頼できないコードを eval するためのセーフレベル4環境とも言えるので eval 出来て当然です。

$SAFE = 4
eval "1 + 1"

なおセーフレベル4環境においてはオブジェクトの参照は許可されていても状態の変更は一般的に許されていないことに注意しておいて下さい。状態の変更が許されるのは汚染されたオブジェクトだけです。例えばクラスに定数を追加することは許されません。

class A; end
$SAFE = 4
class A::B; end #=> Insecure: can't set constant (SecurityError)

ただしクラスAが汚染されている場合には定数を追加することが出来ます。従ってこの例の場合にはクラスAに定数を追加することを許可するために A.taint しておきます。

class A; end

# 汚染させる
A.taint

$SAFE = 4
class A::B; end #=> Insecure: can't set constant (SecurityError)

このようにセーフレベル4環境において状態を変更しなければならない事が分かっているオブジェクトは事前にtaintしておく必要があります。ただしセーフレベル4環境で作成されるオブジェクトは作成された時点で最初から汚染されていますので、これは明示的にtaintしなくても自由に状態を変更できることになります。

$SAFE = 4
s = "abc"
s[-1] = 0
s #=> "ab0"

また事前にオブジェクトを作成しておく必要がある時、いちいちオブジェクトをtaintするのが面倒な場合には、セーフレベル3環境を使うと便利ではないかと思います。

$SAFE = 3
class A; end
$SAFE = 4
class A::B; end

さて、taint/untaint の具合に関してそういう事情ですから、ライブラリの作者様はライブラリ内で安易に taint/untaint しないようご注意下さい。何度かそういうライブラリを見たことがありますが、セーフレベル4環境を導入しづらくなります。

より安全な URI::HTTP#safe_require

以上の説明でセーフレベル4環境の概観と基本的な使い方は理解できたのではないかと思います。ではこの実際的な例として、セーフレベル4環境を利用したより安全な URI::HTTP#safe_require を作成してみましょう。

require "open-uri"

class Sandbox
  def intialize
    self.class.constants.map do |name|
      self.class.instance_eval do
        remove_const name
      end
    end
  end

  def constants
    self.class.constants.map do |name|
      [name, self.class.const_get(name)]
    end
  end

  def env
    binding
  end
end

Sandbox.taint

class URI::HTTP
  def safe_require
    # ファイルをダウンロードして内容を得る
    content = read

    # その内容を評価するのでセーフレベル4環境を導入
    sandbox = Thread.new do
      $SAFE = 3
      sandbox = Sandbox.new
      sandbox.taint
      env = sandbox.env
      $SAFE = 4
      eval(content, env)
      sandbox
    end.value

    # クラスやモジュールをObjectクラスの下に移動
    sandbox.constants.each do |name, body|
      Object.const_set name, body unless Object.const_defined?(name)
    end

    # 真面目にやるならあとはメソッドをKernelに移動とか
  end
end

色々なんだかなぁと思うところがあるかとは思いますが、例示に過ぎないので少々我慢して下さい。ではとにかくこのより安全な URI::HTTP#safe_require を使ってみましょう。まず安全なスクリプトをrequireできることを確かめます。

URI("http://file.dynamic-semantics.com/hatena/20080622-1.rb").safe_require

puts Thread.new {
  $SAFE = 4 # これが必要な理由はすぐ後で説明します
  Foo.new.bar
}.value #=> "Hello, World!"

次に危険なスクリプトをrequireした場合にきちんと例外 SecurityError を発生させることを確かめます。

URI("http://file.dynamic-semantics.com/hatena/20080622-2.rb").safe_require

#=> Insecure operation `delete' at level 4 (SecurityError)

なお次のような危険なスクリプトhttp://file.dynamic-semantics.com/hatena/20080622-3.rb)も検討しておくことにします。

class Foo
  def bar
    File.delete("hello.txt")
  end
end

これは先程と違ってメソッドの中で危険な操作を行なっています。このような場合にはどうなるでしょうか。

URI("http://file.dynamic-semantics.com/hatena/20080622-3.rb").safe_require

puts Thread.new {
  Foo.new.bar #=> calling insecure method: bar (SecurityError)
}.value

このようにセーフレベル4環境において作成されたメソッドがセーフレベル4環境以外から呼び出された時にはSecurityErrorになります(なおこの点はマニュアルに書いてありません。でもセキュリティーモデル的にこの振舞いが妥当なのだろうと思うので、仕様だと思います。ところが、1.8はあれれ?)。なので結局次のようにセーフレベル4環境の中で呼び出す必要があるわけですが、これはもちろんきちんと危険な操作を禁止してくれるので、安心、というわけです。

URI("http://file.dynamic-semantics.com/hatena/20080622-3.rb").safe_require

puts Thread.new {
  $SAFE = 4
  Foo.new.bar # => Insecure operation `delete' at level 4 (SecurityError)
}.value

セーフレベル4環境は信用できるのか

以上の説明で大体セーフレベル4環境にまつわるあれこれは理解できたのではないでしょうか。皆さんセーフレベル4環境はとっても便利だし素敵だと思いませんか。私はとても気に入っています。でも最後にそもそもセーフレベル4環境が信用できるのかどうかについて改めて述べておきます。

拡張ライブラリは基本的に信用できません

非常に残念なことですが、多くの拡張ライブラリはセーフレベルを考慮していません。よってセーフレベル4環境を導入する場合には拡張ライブラリを require しないように心掛けて下さい。もしくはそのライブラリが本当にきちんとセーフレベルを考慮しているかについて精査した上で使用して下さい。拡張ライブラリをrequire した瞬間にセーフレベル4環境が台無し、ということはままあります。気をつけて下さい。

これまで発見されたセーフレベル4環境にとって致命的な脆弱性

非常に残念なことですが、セーフレベル4環境にとって致命的な脆弱性がこれまで何度もみつかっています(過去他にもまだあったりするのでしょうか?)。

ところでセーフレベル4環境にとって致命的な脆弱性とはなんでしょうか。それは一度設定したセーフレベルをどこかで下げられてしまうことです。

$SAFE = 4

# ... eval とか色々 ...

File.delete("hello.txt")
#=> Insecure operation `delete' at level 4 (SecurityError)

# ...のはずが、実は途中で $SAFE = 0 に変更されててファイル消せたら、どうする?

こうしたセーフレベルの降下はセーフレベル情報を適切に伝播しないことによって簡単に起き得ます。Ruby本体であってもセーフレベルの伝播をミスしちゃうくらいなので、拡張ライブラリの作者さんがセーフレベルの伝播をし忘れちゃうとしても無理がない気がします。セーフレベル4のセキュリティーモデルには理論的な問題がないとしても、まぎれもなく実装上の現実的な困難が存在する、ということを忘れてはならないと思います。

まとめ

さて、これで大体セーフレベル4環境にまつわるあれこれは終わりです。今回はセーフレベル4環境の目的、使い方、制限事項、具体例、過去の脆弱性について全体的にざっとまとめてみました。セーフレベル4環境の存在はとても面白くて魅力的です。例示したようにスレッドとProcオブジェクトを組み合わせれば簡単お手軽柔軟に安全な環境を構築できます。しかし一方でその実装を信用し切れないところが重大な問題点であると言えます。例えばウェブブラウザで気軽にIRBを試せる http://tryruby.hobix.com/ はセーフレベル4環境を利用していません。セキュリティーモデルは自前のようです(でも、これはこれで、あれ?と思うような事が出来たりしないことはないような気がなんとなくしました)。こういう場面にこそ本当はセーフレベル4環境を使えればいいのになと思うのですが、やっぱりこういう事情を踏まえれば、なかなかその気にはなれないかも知れません(でも本当の理由はウェブアプリとかIRBとかがセーフレベル4環境と仲が悪いからじゃないかな、と思います、多分)。

まぁ結局のところは、実装が信用できないのなら自分で頑張ってコードをチェックしなさい、ということです。努力はしますが、ただし最も残念なことに私は自分が一番信用できません。というわけなので、つまり何が言いたいかというと、すごいプログラマの皆様に是非とも頑張ってもらいたい!という事です。だってセーフレベル4使いたいんだもん!