プログラミング言語におけるセキュリティサポート
Rubyのセキュリティモデルは、次の二種類の保護を目的としている。
- 信頼できない入力データから、プログラムおよび環境を保護する
- 信頼できないコードから、プログラムの他の部分および環境を保護する
上のことを実現するために、Rubyでは次のようなアプローチをとっている。まず、必要に応じてオブジェクトに汚染マークを付ける。そして汚染マークのつけられたオブジェクトを材料とする一部の操作を制限する。制限の強さには段階があって、その階段のことをセーフレベルと呼んでいる。セーフレベルには低いほうから0、1、2、3、4があり、プログラム中では$SAFEという変数によって指定することができる。セーフレベルは数値が大きいほど制限がきつくなり、しかも、一度上げると下げることができない($SAFEのデフォルト値は0であり、制限はない)。
セーフレベル1では外部からの入力データがすべて汚染される。そして汚染されたオブジェクトを指定しての各種ファイル操作、外部プログラムの実行、evalやrequireなど自プロセスの状態を変え得る操作が禁止される。ただし、プログラム中の操作によって汚染マークを取り除くことは可能で、一度汚染マークを取り除いてしまえば制限の網をくぐりぬけることができる。
次に制限のきついセーフレベル2では、セーフレベル1での制限に加えて、引数が汚染されているかどうかによらずほとんどのファイル操作とforkやkillなどのプロセス関係の操作が禁止される。セーフレベル3では制限がもっと厳しくなり、外部からの入力データであるかどうかによらず、生成されるすべてのオブジェクトに汚染マークが付加され、しかも汚染マークを取り除くことができなくなる。
以上のように1、2、3とレベルが上がるにつれて制限が厳しくなるのだが、その次のセーフレベル4については少々様子が違っている。セーフレベル4なると、セーフレベル3での制限に加えて、すべての入出力が禁止されてしまい、プログラム外部とのやり取りが一切できなくなる。その一方で、セーフレベル3では完全に禁止されているeval(すべてのオブジェクトが汚染されるため)が、条件付きではあるが実行可能となる。
条件というのは、汚染されていないオブジェクトに対して手出しをしない場合に限るというものだ。これによって、汚染されたオブジェクト同士で作用し合う分にはかなりの自由度を与えることが可能となるのと同時に、セーフレベルが3以上に設定される前に作られたオブジェクト(たとえば組み込みのクラスなど)のように汚染マークが付けられていないオブジェクトが、既に汚染されたオブジェクトによって汚されてしまうのを防止できる。このレベルは外部から与えられたコードを実行するための、言ってみれば閉じた領域を作るためのに用意されているものあり、セーフレベル4では外部のコードを安全に実行できるようになると考えられる。
ところで、セーフレベルを指定する$SAFEは、実はスレッドローカル変数である。つまりスレッドごとに異ったレベルを設定することができる。よく考えてみれば、セーフレベル4においては入出力が完全に禁止されているのだから、計算結果の表示をすることすらままならない(以下の実行例のケース1および2)。そこで外部コードを実行するための専用のスレッド生成してセーフレベルを4にする。そして、そのスレッドに対してコードを与え、計算結果を受け取るようにすれば、外部コードの安全な実行と結果の出力の両方が可能となる(実行例のケース3および4)。
[ケース1] 外部からのコードを読み込めない(間違った用例) $ echo '1 + 1' | ruby -e ' $SAFE = 4 # セーフレベルを4に設定 code = STDIN.read # 外部コードを読み込む ret = eval(code) # 外部コードを評価する puts ret # codeの評価結果を表示する' -e:3:in `read': Insecure: operation on untainted IO (SecurityError) from -e:3 [ケース2] 計算結果を表示することができない(間違った用例) $ echo '1 + 1' | ruby -e ' code = STDIN.read # 外部コードを読み込む $SAFE = 4 # セーフレベルを4に設定 ret = eval(code) # 外部コードを評価する puts ret # codeの評価結果を表示する' -e:5:in `write': Insecure operation `write' at level 4 (SecurityError) from -e:5:in `puts' from -e:5 [ケース3] 外部からコードを読み込み、安全に評価できた例(計算結果が表示されている) $ echo '1 + 1' | ruby -e ' code = STDIN.read # 外部コードを読み込む th = Thread.new { # スレッドを生成する $SAFE = 4 # セーフレベルを4に設定 eval(code) # 外部コードを評価する } puts th.value # codeの評価結果を表示する' 2 [ケース4] 外部からコードを読み込み、危険なコードを検出した例(lsコマンドは実行されない) $ echo '`ls`' | ruby -e ' code = STDIN.read # 外部コードを読み込む th = Thread.new { # スレッドを生成する $SAFE = 4 # セーフレベルを4に設定 eval(code) # 外部コードを評価する } puts th.value # codeの評価結果を表示する' -e:5: (eval):1:in ``': Insecure operation - ` (SecurityError) from -e:6:in `value' from -e:6
以上のように、Rubyのセキュリティモデルは、究極的にはサンドボックスを目指すものであると言えるだろう。ただし複雑な状況においてどのように機能するかや、よりよい在り方については今もなお議論の余地があるとされている。もっとも、現状でも、ほとんどの「うっかり」ミスを防ぐことはできるであろうし、システムの運用管理上、致命的な状況になることも防げるだろう。もちろん故意にセーフレベルを低いままにしていたり、十分なチェックなしに汚染マークを除去してしまっていたりするのは論外である。
最後に注意点を挙げておこう。Rubyのみで書かれたコードであれば、上で説明した機構と同じ水準の保護がなされるが、C言語などで記述されたRuby用の拡張ライブラリについては保護が及ばない。ある拡張ライブラリで提供される機能が、各セーフレベルで想定されるような動作をするかどうかは、その拡張ライブラリの作者が十分に注意深くプログラムをしたかどうかによる。標準添付の拡張ライブラリについては問題なかろうが(問題があればバグである)、サードパーティの拡張ライブラリについては注意が必要である。