RestrictedPython。

RedashやPloneで使われているRestrictedPython。使う機会あったのでまとめる。

とりわけ、自前RestrictionTransformerクラスによるpolicyの設定方法は情報がネット上に皆無だったので、残しておけれればと思う次第。

...諸兄姉においては、RestrictedPythonさらにはexec、evalを使わざるおえない局面が来ないことを祈る(セキュリティ観点で)

環境

RestrictedPython

概要

Pythonソースコード文字列の実行時評価(e.g. exec、eval)する際に、評価するコードに制約(e.g. for statementの禁止)をかけるためのライブラリ。

CPython 2.7、3.4、3.5、3.6がサポートされている。PyPyをはじめとしたCPython以外のPython処理系は、サポート外。*1

注意点・観点

第一に、exec、evalは使うのは避けるのがよい。

理由としては、安全性・セキュリティの観点から、そもそもexec、evalは使うのは避けるるべきである。

避ける方法としては、VMを実装するのが一つ。
それ以外にも、ast.literal_eval使う方法や、別プロセスやコンテナの中など、限定された環境で評価を行うのがよい。

もし、言語ランタイムがサンボドックス実行の手段を提供してくれているのであれば、検討すること。

たとえば、言語ランタイムが提供するサンドボックスで知っている限りだと、Javaアプレット時代のサンドボックスや、PyPyが実験的に実装したサンドボックス*2

インストール

pip install RestrictedPython

今回は、4.0βで試した。

pip install RestrictedPython==4.0b4

from RestrictedPython import compile_restricted

source_code = """                                                               
def add(x):                                                                     
  return x + 1                                                                  
"""

context = {}
try:
    byte_code = compile_restricted(source_code, '<inline>', 'exec')
    exec(byte_code, None, context)
except SyntaxError as e:
    raise e
assert 2 == context['add'](1)

制約をかける

文・式の制約を加えたい。
RestrictedPythonではPolicyという概念があり、事前定義された組み込み関数が3つ(safe_builtins、limited_builtins、utility_builtins)を使うことで制約をかけられる。

safe_builtinsを使う

execの第二引数のdictで__buildins__にsafe_builtinsを設定する。

from RestrictedPython import compile_restricted
from RestrictedPython import safe_builtins

source_code = """                                                               
def add(x):                                                                     
  return x + 1                                                                  
"""

context = {}
try:
    byte_code = compile_restricted(source_code, '<inline>', 'exec')
    exec(byte_code, {'__builtins__': safe_builtins}, context)
except SyntaxError as e:
    raise e
assert 2 == context['add'](1)
safe_builtinsで制約がかけられているimport文を呼び出す

さて、safe_builtinsでは、import文が禁止されている。
試してみよう。

from RestrictedPython import compile_restricted
from RestrictedPython import safe_builtins

source_code = "import this"

context = {}
try:
    byte_code = compile_restricted(source_code, '<inline>', 'exec')
    exec(byte_code, {'__builtins__': safe_builtins}, context)
except SyntaxError as e:
    raise e

上記を実行した際はImportErrorが排出される。

ImportError: __import__ not found
本題。Policyに自前のRestrictingNodeTransformerを指定し制約をかける

RestrictedPythonの提供するcompile関数群(compile_restricted、compile_restricted_exec、compile_restricted_eval、compile_restricted_single)は、policyにRestrictingNodeTransformerを指定できる。

RestrictedPythonはRestrectedPytohnが提供している。親クラスは、標準モジュールastが提供するast.NodeTransformer。このクラスを継承して、自前のクラスを作ることによって制約をかけることができる。

例えば、import文を自前のOwnRestrictingNodeTransformerクラスで禁止したい場合は下記。

from RestrictedPython import compile_restricted
from RestrictedPython import RestrictingNodeTransformer

class OwnRestrictingNodeTransformer(RestrictingNodeTransformer):
    def visit_Import(self, node):
        self.error(node, 'Import statements are not allowed.')
    visit_ImportFrom = visit_Import

source_code = "import this"

try:
    byte_code = compile_restricted(
        source=source_code,
        filename='<inline>',
        mode='exec',
        policy=OwnRestrictingNodeTransformer)
    exec(byte_code)
except SyntaxError as e:
    raise e

実行すると下記となる。

SyntaxError: ('Line 1: Import statements are not allowed.',)

この要領で文、式の制約をかければいい。むろん特定のモジュールがインポートされた場合に対してだってなんだってやりたい放題。Pythonのビルドインライブラリast様様。

私感

astモジュール駆使して、制約かけるのはgoodではあるが...やはりそもそも論として、RestrictedPythonさらにはexec、evalを使わざるおえない局面が来ないことを祈る(セキュリティ観点で)

それるが、RestrictedPythonは、Zope Foundationが提供している。個人的な推測だが、Ploneで使う前提で作られ、ライブラリとして切り出されたと思われる。

Pythonを使ってるとZopeは避けられない。

*1:ソースコードぱっと見た限りはPyPyのサポートを頑張っている痕跡はある

*2:PyPy’s sandboxing features — PyPy documentation。現状使える状態なのかは不明。