Pythonのイントロスペクション機能を使用して物事を掘り下げて再抽出できるため、危険な関数を非表示にしても、 eval
は危険 であることは誰もが知っています。たとえば、__builtins__
を削除しても、次のようにして取得できます
[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == 'catch_warnings'][0]()._module.__builtins__
ただし、これについて私が見たすべての例では、属性アクセスを使用しています。 および属性アクセスをすべて無効にした場合(Python tokenizerで入力をトークン化して拒否することにより)属性アクセストークンがある場合)?
そして、あなたが尋ねる前に、いいえ、私のユースケースでは、これらのどちらも必要ないので、それほど不自由ではありません。
私がやろうとしていることは、SymPyの sympify 関数をより安全にすることです。現在、入力をトークン化し、いくつかの変換を行い、名前空間で評価します。ただし、属性へのアクセスを許可するため、安全ではありません(実際には必要ありません)。
Python 3.6- f-strings 。
彼らは式を評価でき、
>>> eval('f"{().__class__.__base__}"', {'__builtins__': None}, {})
"<class 'object'>"
ただし、属性アクセスはPythonのトークナイザでは検出されません。
0,0-0,0: ENCODING 'utf-8'
1,0-1,1: ERRORTOKEN "'"
1,1-1,27: STRING 'f"{().__class__.__base__}"'
2,0-2,0: ENDMARKER ''
eval
から戻り値を作成して、 exception outsideeval
をスローしようとした場合、print
、log
、repr
、何でも:
eval('''((lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args))))
(lambda f: lambda n: (1,(1,(1,(1,f(n-1))))) if n else 1)(300))''')
これにより、(1,(1,(1,(1...
という形式のネストされたタプルが作成されます。その値をprint
ed(Python 3の場合)、str
edまたはrepr
edにすることはできません。それをデバッグしようとするすべての試みは
RuntimeError: maximum recursion depth exceeded while getting the repr of a Tuple
pprint
とsaferepr
も失敗します:
...
File "/usr/lib/python3.4/pprint.py", line 390, in _safe_repr
orepr, oreadable, orecur = _safe_repr(o, context, maxlevels, level)
File "/usr/lib/python3.4/pprint.py", line 340, in _safe_repr
if issubclass(typ, dict) and r is dict.__repr__:
RuntimeError: maximum recursion depth exceeded while calling a Python object
したがって、これを文字列化する安全な組み込み関数はありません。次のヘルパーが役立つ可能性があります。
def excsafe_repr(obj):
try:
return repr(obj)
except:
return object.__repr__(obj).replace('>', ' [exception raised]>')
そして、print
in Python 2が実際にstr
を使用しないという問題があります。/repr
、再帰チェックがないため安全ではありません。つまり、上記のラムダモンスターの戻り値を取得します。str
、repr
はできませんが、通常のprint
(print_function
ではない)が出力します。うまく。ただし、print
ステートメントを使用して出力されることがわかっている場合は、これを利用してPython 2にSIGSEGVを生成できます。
print eval('(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)')
クラッシュPython 2 with SIGSEGV。 バグトラッカーの [〜#〜] wontfix [〜#〜] です。したがって、安全を確保したい場合は、print
- the-statementを使用しないでください。 from __future__ import print_function
!
これはクラッシュではありませんが、
eval('(1,' * 100 + ')' * 100)
実行すると、出力
s_Push: parser stack overflow
Traceback (most recent call last):
File "yyy.py", line 1, in <module>
eval('(1,' * 100 + ')' * 100)
MemoryError
MemoryError
はキャッチでき、Exception
のサブクラスです。パーサーにはいくつかの stackoverflowsからのクラッシュを回避するために非常に保守的な制限 (意図されたしゃれ)があります。ただし、s_Push: parser stack overflow
はCコードによってstderr
に出力され、抑制できません。
そして昨日私は尋ねました なぜクラッシュしないようにPython 3.4を修正しないのですか 、
% python3
Python 3.4.3 (default, Mar 26 2015, 22:03:40)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
... def f(self):
... nonlocal __x
...
[4] 19173 segmentation fault (core dumped) python3
および Serhiy Storchakaの回答 Pythonコア開発者は、整形式に見えるコードのSIGSEGVをセキュリティ問題と見なさないことを確認しました。
3.4では、セキュリティ修正のみが受け入れられます。
したがって、サニタイズされているかどうかにかかわらず、サードパーティからのコードをPythonで実行することは決して安全であるとは決して言えないと結論付けることができます。
そして Nick Coghlan そして added :
また、Pythonコードによって引き起こされたセグメンテーション違反が現在セキュリティバグと見なされない理由に関するいくつかの追加の背景として:CPythonにはセキュリティサンドボックスが含まれていないため、提供するOSに完全に依存していますプロセスの分離。そのOSレベルのセキュリティ境界は、コードが「正常に」実行されているのか、または意図的にトリガーされたセグメンテーション違反の後で変更された状態で実行されているのかには影響されません。
ユーザーは引き続き、膨大な数に評価される式を入力することでDoSを実行できます。これにより、メモリがいっぱいになり、Pythonプロセスがクラッシュします。たとえば、
'10**10**100'
ビルトインの回復やsegfaultの作成など、より伝統的な攻撃が可能かどうか、私は間違いなく興味があります。
編集:
結局のところ、Pythonのパーサーでさえこの問題があります。
lambda: 10**10**100
定数を事前計算しようとするため、ハングします。
私は信じていませんPythonは信頼できないコードに対するセキュリティを備えているように設計されています。公式のPython 2インタプリタ:
eval('()' * 98765)
私の answer から「SIGSEGVを返す最も短いコード」コードゴルフの質問へ。
評価された式に安全でないトークンが含まれていないことを確認するsafe_evalの例を次に示します。 ASTを解釈するliteral_evalのアプローチをとるのではなく、トークンタイプをホワイトリストに登録し、式がテストに合格した場合は実際のevalを使用します。
# license: MIT (C) tardyp
import ast
def safe_eval(expr, variables):
"""
Safely evaluate a a string containing a Python
expression. The string or node provided may only consist of the following
Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
and None. safe operators are allowed (and, or, ==, !=, not, +, -, ^, %, in, is)
"""
_safe_names = {'None': None, 'True': True, 'False': False}
_safe_nodes = [
'Add', 'And', 'BinOp', 'BitAnd', 'BitOr', 'BitXor', 'BoolOp',
'Compare', 'Dict', 'Eq', 'Expr', 'Expression', 'For',
'Gt', 'GtE', 'Is', 'In', 'IsNot', 'LShift', 'List',
'Load', 'Lt', 'LtE', 'Mod', 'Name', 'Not', 'NotEq', 'NotIn',
'Num', 'Or', 'RShift', 'Set', 'Slice', 'Str', 'Sub',
'Tuple', 'UAdd', 'USub', 'UnaryOp', 'boolop', 'cmpop',
'expr', 'expr_context', 'operator', 'slice', 'unaryop']
node = ast.parse(expr, mode='eval')
for subnode in ast.walk(node):
subnode_name = type(subnode).__name__
if isinstance(subnode, ast.Name):
if subnode.id not in _safe_names and subnode.id not in variables:
raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode.id))
if subnode_name not in _safe_nodes:
raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode_name))
return eval(expr, variables)
class SafeEvalTests(unittest.TestCase):
def test_basic(self):
self.assertEqual(safe_eval("1", {}), 1)
def test_local(self):
self.assertEqual(safe_eval("a", {'a': 2}), 2)
def test_local_bool(self):
self.assertEqual(safe_eval("a==2", {'a': 2}), True)
def test_lambda(self):
self.assertRaises(ValueError, safe_eval, "lambda : None", {'a': 2})
def test_bad_name(self):
self.assertRaises(ValueError, safe_eval, "a == None2", {'a': 2})
def test_attr(self):
self.assertRaises(ValueError, safe_eval, "a.__dict__", {'a': 2})
def test_eval(self):
self.assertRaises(ValueError, safe_eval, "eval('os.exit()')", {})
def test_exec(self):
self.assertRaises(SyntaxError, safe_eval, "exec 'import os'", {})
def test_multiply(self):
self.assertRaises(ValueError, safe_eval, "'s' * 3", {})
def test_power(self):
self.assertRaises(ValueError, safe_eval, "3 ** 3", {})
def test_comprehensions(self):
self.assertRaises(ValueError, safe_eval, "[i for i in [1,2]]", {'i': 1})
locals
およびglobals
辞書の制御は非常に重要です。それ以外の場合、誰かがeval
またはexec
を渡して再帰的に呼び出すことができます
safe_eval('''e("""[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""")''',
globals={'e': eval})
再帰的なeval
の式は単なる文字列です。
また、グローバルネームスペースのeval
およびexec
名を、実際のeval
またはexec
ではないものに設定する必要があります。グローバル名前空間は重要です。ローカル名前空間を使用する場合、内包表記やラムダなど、別の名前空間を作成するものはすべて、それを回避します
safe_eval('''[eval("""[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""") for i in [1]][0]''', locals={'eval': None})
safe_eval('''(lambda: eval("""[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__"""))()''',
locals={'eval': None})
ここでも、safe_eval
は文字列と関数呼び出しのみを認識し、属性アクセスは認識しません。
安全な解析を無効にするフラグがある場合は、safe_eval
関数自体をクリアする必要もあります。そうでなければ、あなたは単に行うことができます
safe_eval('safe_eval("<dangerous code>", safe=False)')