LoginSignup
12
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-31

はじめに

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

※本記事の内容は以下の内容を噛み砕いてまとめたものです。
Eval really is dangerous
Redditの関連する議論

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

実験環境

  • Python 3.6.8

何が危険なのか

evalは、次のように文字列を式として評価します。

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

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

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

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

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

ただし、この第2引数に与えた辞書に__builtins__というキーが含まれていない場合は例外的に、キー__builtins__に対して組み込みモジュールbuiltinsの辞書が値として自動的に挿入される仕様になっています。
公式ドキュメント)

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

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

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

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

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

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特有のことなのかどうかは正直よくわかりません)

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

どうしたらいいか

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

  • ast.literal_eval()
  • pyparsing

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

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7