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で使う前提で作られ、ライブラリとして切り出されたと思われる。
参考
- Sandboxing and RestrictedPython — Plone Documentation v5.1
- GitHub - zopefoundation/RestrictedPython: A restricted execution environment for Python to run untrusted code.
- RestrictedPython · PyPI
- https://docs.python.jp/3/library/functions.html#exec
- 32.2. ast --- 抽象構文木 — Python 3.6.5 ドキュメント #ast.NodeTransformer
- 32.2. ast --- 抽象構文木 — Python 3.6.5 ドキュメント #abstract-grammar
- Policyの詳細は、公式資料の Policies & builtins — RestrictedPython 4.0.0.dev0 documentation を参考。ただし、薄い
- nexcedi.comの https://lab.nexedi.com/Daetalus/RestrictedPython-3.6.0/blob/c4652cb365edfe21e41c20bdf51c92dfab7a283f/src/RestrictedPython/RestrictionMutator.py#L65 は数少ないサンプルとなるコード
- Doing the Impossible — Plone Conference 2017 – Barcelona
*1:ソースコードぱっと見た限りはPyPyのサポートを頑張っている痕跡はある
*2:PyPy’s sandboxing features — PyPy documentation。現状使える状態なのかは不明。