Pythonの組み込み関数eval・execのセキュリティリスクについて

# はじめに

Pythonの組み込み関数`eval`・`exec`は、文字列をPythonコードとして評価・実行してくれる便利な関数です。
例えばユーザが入力したコードをインタラクティブに実行するWEBサービスなんかを作ったりすることにも使えそうなように思えます。
が、この機能、気をつけて使わないと場合によってはシステムの重大な脆弱性になり得ます。
少なくとも、信頼できない入力を受け取って`eval`・`exec`に投げるのはご法度である、という話です。

※本記事の内容は以下の内容を噛み砕いてまとめたものです。
[Eval really is dangerous](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html)
[Redditの関連する議論](https://www.reddit.com/r/Python/comments/hftnp/ask_rpython_recovering_cleared_globals/)

`eval`についても`exec`についてもほぼ同様の議論になるので、以下では特に`evel`について述べます。

## 実験環境
- Python 3.6.8

# 何が危険なのか
`eval`は、次のように文字列を式として評価します。

```python
assert eval("2 + 3 * len('hello')") == 17
```

仮に`os`モジュールが外側のソースコード内でimportされている場合、以下のような文字列を与えられるとこれはそのまま評価され、害を及ぼすシステムコマンドも実行できてしまいます。

```python
eval("os.remove('file.txt')")
# 'file.txt'が削除されてしまう
```

これに対して、`eval`内で参照できる名前空間を制限するという対策が考えられます。
`eval`の第2引数として辞書を与えると、評価時のグローバル名前空間がその辞書の中身に置き換えられます。よって、空の辞書を与えれば上記の「os」モジュールを使用できなくなります。

```python
eval("os.remove('file.txt')", {})
# NameError: name 'os' is not defined
```

ただし、この第2引数に与えた辞書に`__builtins__`というキーが含まれていない場合は例外的に、キー`__builtins__`に対して組み込みモジュールbuiltinsの辞書が値として自動的に挿入される仕様になっています。
（[公式ドキュメント](https://docs.python.org/ja/3.6/library/functions.html#eval))

`__builtins__`というのは通常組み込みオブジェクトにアクセスするために自動的に定義されるグローバル変数です。
ここにbuiltinsモジュールの中身が自動的にセットされるとはつまり、組み込み関数については通常時と同様に使用できるということを意味します。
`import`文を呼び出したときに内部的に呼び出される組み込み関数`__import__()`にもアクセスできることになり、これによって結局組み込み関数以外にも任意の関数を以下のように実行できるということになります。

```python
eval("__import__('os').remove('file.txt')", {})
# 削除は実行される
```

これを防ぐ方法は、`__builtins__`キーに対して明示的に値を設定してこの穴を塞ぐことです。こうすれば、通常の方法では組み込みオブジェクトにアクセスすることができなくなります。

```python
eval("__import__('os').remove('file.txt')", {'__builtins__': {}})
# NameError: name '__import__' is not defined
```

これで一安心、かと思いきや、そうではありません。
直接組み込みオブジェクトにアクセスできなくても、実は例えば以下のように穴を突くと同様のことができてしまいます。

```python
eval("""
[
    c for c in ().__class__.__base__.__subclasses__()
    if c.__name__ == 'catch_warnings'
][0]()._module.__builtins__['__import__']('os').remove('file.txt')
""", 
     {'__builtins__': {}})
# 削除は実行される
```

一見なんのことやらわかりませんが、中身を順に見てみましょう。
まず`().__class__.__base__`の部分に注目します。ここでやりたいことは要は「objectクラスへのアクセス」です。
組み込みオブジェクトへのアクセスができないために、直接「object」を呼ぶのではなく`().__class__`でtupleクラスを呼んでその基底クラスにアクセスする、という回りくどい手順を踏んでいます。
`object`へアクセスできたら、そこから`__subclasses__()`を呼ぶことで全サブクラスのリストを取得できます。objectはpythonの全クラスの基底クラスですから、これはつまりこの時点で存在するすべてのクラスへアクセスできるということを意味します。
これができれば、`[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'xxxxx'][0]`で、任意のクラスの名前を指定して取得できます。
ここでは、`catch_warnings`クラスのオブジェクトを生成して`_module`を呼ぶと標準ライブラリ`warnings`のモジュール自体にアクセスでき、ここから`__builtins__`を復元して、任意の組み込み関数を呼び出すことを達成しています。
（このあたりはwarnings特有のことなのかどうかは正直よくわかりません）

これに対して、ではどうすればいいのか……について上記記事では、例えば入力されたソースに`__`（ダブルアンダースコア）が含まれていた場合は実行を拒否するなどとすれば__おそらく__安全になるのではないか、という提案で締められています。
ただしこういったブラックリストを作って穴を塞いでいくような対策では、思わぬ穴が発見されてすり抜けられる可能性をゼロにすることはできず、真に安全になっているとは言えない、というのがいったんの結論です。

# どうしたらいいか
調べた限りでは、不特定多数からの信頼できないソースについて`eval`・`exec`を安全に使用することは難しそうです（良い方法があれば教えて下さい）。
代替案として、できることは制限されますが

- ast.literal_eval()
- pyparsing

あたりで所望の動作を実現できないかを検討するのが現実的なようです。
一方で、入力が信頼できるソースからのものに限られると断言できる場合であれば、`eval`・`exec`を便利に使えばよいと思います。

# まとめ
- `eval`・`exec`の標準機能による名前空間の制限は、セキュリティ上の防御にはなり得ない
- 不特定多数からの信頼できない入力を受け付けるシステムでは、`eval`・`exec`を使うべきでない
- ブラックリストによるセキュリティ対策では確実な安全性は獲得できない