# 30.1. `rexec` --- 制限実行のフレームワーク

バージョン 2.6 で非推奨: `rexec` モジュールは Python 3 で削除されました。

バージョン 2.3 で変更: モジュールは無効化され、使えなくなりました。

警告

このドキュメントは、 `rexec` モジュールを使用している古いコードを読む際の参照用として残されています。

このモジュールには `RExec` クラスが含まれています。このクラスは、 `r_eval()` 、 `r_execfile()` 、 `r_exec()` および `r_import()` メソッドをサポートし、これらは標準の Python 関数 `eval()` 、 `execfile()` および `exec` と `import` 文の制限されたバージョンです。この制限された環境で実行されるコードは、安全であると見なされたモジュールや関数だけにアクセスします; `RExec` をサブクラス化すれば、望むように能力を追加および削除できます。

警告

`rexec` モジュールは、下記のように動作するべく設計されてはいますが、注意深く書かれたコードなら利用できてしまうかもしれない、既知の脆弱性がいくつかあります。従って、"製品レベル" のセキュリティを要する状況では、 `rexec` の動作をあてにするべきではありません。製品レベルのセキュリティを求めるなら、サブプロセスを介した実行や、あるいは処理するコードとデータの両方に対する非常に注意深い "浄化" が必要でしょう。上記の代わりに、 `rexec` の既知の脆弱性に対するパッチ当ての手伝いも歓迎します。

注釈

`RExec` クラスは、プログラムコードによるディスクファイルの読み書きや TCP/IP ソケットの利用といった、安全でない操作の実行を防ぐことができます。しかし、プログラムコードよる非常に大量のメモリや処理時間の消費に対して防御することはできません。

class `rexec.``RExec`([hooks[, verbose]])

  `RExec` クラスのインスタンスを返します。

  hooks は、 `RHooks` クラスあるいはそのサブクラスのインスタンスです。 hooks が省略されているか `None` であれば、デフォルトの `RHooks` クラスがインスタンス化されます。 `rexec` モジュールが (組み込みモジュールを含む) あるモジュールを探したり、あるモジュールのコードを読んだりする時は常に、 `rexec` がじかにファイルシステムに出て行くことはありません。その代わり、あらかじめ `RHooks` クラスに渡しておいたり、コンストラクタで生成された `RHooks` インスタンスのメソッドを呼び出します。 (実際には、 `RExec` オブジェクトはこれらを呼び出しません --- 呼び出しは、 `RExec` オブジェクトの一部であるモジュールローダオブジェクトによって行われます。これによって別のレベルの柔軟性が実現されます。この柔軟性は、制限された環境内で `import` 機構を変更する時に役に立ちます。 )

  代替の `RHooks` オブジェクトを提供することで、モジュールをインポートする際に行われるファイルシステムへのアクセスを制御することができます。このとき、各々のアクセスが行われる順番を制御する実際のアルゴリズムは変更されません。例えば、 `RHooks` オブジェクトを置き換えて、ILU のようなある種の RPC メカニズムを介することで、全てのファイルシステムの要求をどこかにあるファイルサーバに渡すことができます。 Grail のアプレットローダは、アプレットを URL からディレクトリ上に import する際にこの機構を使っています。

  もし verbose が true であれば、追加のデバッグ出力が標準出力に送られます。

制限された環境で実行するコードも、やはり `sys.exit()` 関数を呼ぶことができることを知っておくことは大事なことです。制限されたコードがインタプリタから抜けだすことを許さないためには、いつでも、制限されたコードが、 `SystemExit` 例外をキャッチする `try` / `except` 文とともに実行するように、呼び出しを防御します。制限された環境から `sys.exit()` 関数を除去するだけでは不十分です -- 制限されたコードは、やはり `raise SystemExit` を使うことができてしまいます。 `SystemExit` を取り除くことも、合理的なオプションではありません; いくつかのライブラリコードはこれを使っていますし、これが利用できなくなると中断してしまうでしょう。

参考

Grail Home Page

  Grail はすべて Python で書かれた Web ブラウザです。これは、 `rexec` モジュールを、Python アプレットをサポートするのに使っていて、このモジュールの使用例として使うことができます。

## 30.1.1. RExec オブジェクト

`RExec` インスタンスには以下のメソッドがあります:

`RExec.``r_eval`(code)

  code は、Python の式を含む文字列か、あるいはコンパイルされたコードオブジェクトのどちらかでなければなりません。そしてこれらは制限された環境の `__main__` モジュールで評価されます。式あるいはコードオブジェクトの値が返されます。

`RExec.``r_exec`(code)

  code は、1行以上の Python コードを含む文字列か、コンパイルされたコードオブジェクトのどちらかでなければなりません。そしてこれらは、制限された環境の `__main__` モジュールで実行されます。

`RExec.``r_execfile`(filename)

  ファイル filename 内の Python コードを、制限された環境の `__main__` モジュールで実行します。

名前が `s_` で始まるメソッドは、 `r_` で始まる関数と同様ですが、そのコードは、標準 I/O ストリーム `sys.stdin` 、 `sys.stderr` および `sys.stdout` の制限されたバージョンへのアクセスが許されています。

`RExec.``s_eval`(code)

  code は、Python 式を含む文字列でなければなりません。そして制限された環境で評価されます。

`RExec.``s_exec`(code)

  code は、1 行以上の Python コードを含む文字列でなければなりません。そして制限された環境で実行されます。

`RExec.``s_execfile`(code)

  ファイル filename に含まれた Python コードを制限された環境で実行します。

`RExec` オブジェクトは、制限された環境で実行されるコードによって暗黙のうちに呼ばれる、さまざまなメソッドもサポートしなければなりません。これらのメソッドをサブクラス内でオーバライドすることによって、制限された環境が強制するポリシーを変更します。

`RExec.``r_import`(modulename[, globals[, locals[, fromlist]]])

  モジュール modulename をインポートし、もしそのモジュールが安全でないとみなされるなら、 `ImportError` 例外を発生します。

`RExec.``r_open`(filename[, mode[, bufsize]])

  `open()` が制限された環境で呼ばれるとき、呼ばれるメソッドです。引数は `open()` のものと同じであり、ファイルオブジェクト (あるいはファイルオブジェクトと互換性のあるクラスインスタンス) が返されます。 `RExec` のデフォルトの動作は、任意のファイルを読み取り用にオープンすることを許可しますが、ファイルに書き込もうとすることは許しません。より制限の少ない `r_open()` の実装については、以下の例を見て下さい。

`RExec.``r_reload`(module)

  モジュールオブジェクト module を再ロードして、それを再解析し再初期化します。

`RExec.``r_unload`(module)

  モジュールオブジェクト module をアンロードします (それを制限された環境の `sys.modules` 辞書から取りのぞきます)。

および制限された標準 I/O ストリームへのアクセスが可能な同等のもの:

`RExec.``s_import`(modulename[, globals[, locals[, fromlist]]])

  モジュール modulename をインポートし、もしそのモジュールが安全でないとみなされるなら、 `ImportError` 例外を発生します。

`RExec.``s_reload`(module)

  モジュールオブジェクト module を再ロードして、それを再解析し再初期化します。

`RExec.``s_unload`(module)

  モジュールオブジェクト module をアンロードします。

## 30.1.2. 制限された環境を定義する

`RExec` クラスには以下のクラス属性があります。それらは、 `__init__()` メソッドが使います。それらを既存のインスタンス上で変更しても何の効果もありません; そうする代わりに、 `RExec` のサブクラスを作成して、そのクラス定義でそれらに新しい値を割り当てます。そうすると、新しいクラスのインスタンスは、これらの新しい値を使用します。これらの属性のすべては、文字列のタプルです。

`RExec.``nok_builtin_names`

  制限された環境で実行するプログラムでは利用 できない であろう、組み込み関数の名前を格納しています。 `RExec` に対する値は、 `('open', 'reload', '__import__')` です。 (これは例外です。というのは、組み込み関数のほとんど大多数は無害だからです。この変数をオーバライドしたいサブクラスは、基本クラスからの値から始めて、追加した許されない関数を連結していかなければなりません -- 危険な関数が新しく Python に追加された時は、それらも、このモジュールに追加します。)

`RExec.``ok_builtin_modules`

  安全にインポートできる組み込みモジュールの名前を格納しています。 `RExec` に対する値は、 `('audioop', 'array', 'binascii', 'cmath', 'errno', 'imageop', 'marshal', 'math', 'md5', 'operator', 'parser', 'regex', 'select', 'sha', '_sre', 'strop', 'struct', 'time')` です。この変数をオーバーライドする場合も、同様な注意が適用されます -- 基本クラスからの値を使って始めます。

`RExec.``ok_path`

  `import` が制限された環境で実行される時に検索されるディレクトリーを格納しています。 `RExec` に対する値は、(モジュールがロードされた時は) 制限されないコードの `sys.path` と同一です。

`RExec.``ok_posix_names`

  制限された環境で実行するプログラムで利用できる、 `os` モジュール内の関数の名前を格納しています。 `RExec` に対する値は、 `('error', 'fstat', 'listdir', 'lstat', 'readlink', 'stat', 'times', 'uname', 'getpid', 'getppid', 'getcwd', 'getuid', 'getgid', 'geteuid', 'getegid')` です。

`RExec.``ok_sys_names`

  制限された環境で実行するプログラムで利用できる、 `sys` モジュール内の関数名と変数名を格納しています。 `RExec` に対する値は、 `('ps1', 'ps2', 'copyright', 'version', 'platform', 'exit', 'maxint')` です。

`RExec.``ok_file_types`

  モジュールがロードすることを許されているファイルタイプを格納しています。各ファイルタイプは、 `imp` モジュールで定義された整数定数です。意味のある値は、 `PY_SOURCE` 、 `PY_COMPILED` および `C_EXTENSION` です。 `RExec` に対する値は、 `(C_EXTENSION, PY_SOURCE)` です。サブクラスで `PY_COMPILED` を追加することは推奨されません; 攻撃者が、バイトコンパイルしたでっちあげのファイル(`.pyc`)を、例えば、あなたの公開 FTP サーバの `/tmp` に書いたり、 `/incoming` にアップロードしたりして、とにかくあなたのファイルシステム内に置くことで、制限された実行モードから抜け出ることができるかもしれないからです。

## 30.1.3. 例

標準の `RExec` クラスよりも、若干、もっと緩めたポリシーを望んでいるとしましょう。例えば、もし `/tmp` 内のファイルへの書き込みを喜んで許すならば、 `RExec` クラスを次のようにサブクラス化できます:

    class TmpWriterRExec(rexec.RExec):
        def r_open(self, file, mode='r', buf=-1):
            if mode in ('r', 'rb'):
                pass
            elif mode in ('w', 'wb', 'a', 'ab'):
                # check filename: must begin with /tmp/
                if file[:5]!='/tmp/':
                    raise IOError("can't write outside /tmp")
                elif (string.find(file, '/../') >= 0 or
                     file[:3] == '../' or file[-3:] == '/..'):
                    raise IOError("'..' in filename forbidden")
            else: raise IOError("Illegal open() mode")
            return open(file, mode, buf)

上のコードは、完全に正しいファイル名でも、時には禁止する場合があることに注意して下さい; 例えば、制限された環境でのコードでは、 `/tmp/foo/../bar` というファイルはオープンできないかもしれません。これを修正するには、 `r_open()` メソッドが、そのファイル名を `/tmp/bar` に単純化しなければなりません。そのためには、ファイル名を分割して、それにさまざまな操作を行う必要があります。セキュリティが重大な場合には、より複雑で微妙なセキュリティホールを抱え込むかもしれない一般性のあるコードよりも、制限が余りにあり過ぎるとしても単純なコードを書く方が、望ましいでしょう。