Wir alle wissen, dass eval
gefährlich ist , auch wenn Sie gefährliche Funktionen ausblenden, da Sie Pythons Introspektionsfunktionen verwenden können, um nach Dingen zu suchen und sie erneut zu extrahieren. Selbst wenn Sie beispielsweise __builtins__
löschen, können Sie sie mit abrufen
[c for c in ().__class__.__base__.__subclasses__()
if c.__== 'catch_warnings'][0]()._module.__builtins__
Jedes Beispiel, das ich gesehen habe, verwendet jedoch Attributzugriff. Was ist, wenn ich alle integrierten Funktionen deaktiviere, und den Attributzugriff deaktiviere (indem ich die Eingabe mit einem Python-Tokenizer toke und ablehne, wenn sie ein Attributzugriffstoken enthält)?
Und bevor Sie fragen, nein, für meinen Anwendungsfall brauche ich keines von beiden, damit es nicht zu lähmend ist.
Ich versuche, die sympify - Funktion von SymPy sicherer zu machen. Gegenwärtig wird die Eingabe mit einem Token versehen, es werden einige Transformationen ausgeführt und sie werden in einem Namespace ausgewertet. Es ist jedoch unsicher, da es den Attributzugriff ermöglicht (obwohl es ihn wirklich nicht benötigt).
Ich werde eine der neuen Funktionen von Python 3.6 - f-strings erwähnen.
Sie können Ausdrücke auswerten,
>>> eval('f"{().__class__.__base__}"', {'__builtins__': None}, {})
"<class 'object'>"
der Attributzugriff wird jedoch nicht von Pythons Tokenizer erkannt:
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 ''
Es ist möglich, aus eval
einen Rückgabewert zu konstruieren, der ein Ausnahmeauslöst. _ outside eval
, wenn Sie versuchen, print
, log
, repr
, irgendetwas:
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))''')
Dadurch wird ein verschachteltes Tupel der Form (1,(1,(1,(1...
erstellt. dieser Wert kann nicht print
ed (auf Python 3), str
ed oder repr
ed sein; Alle Versuche, es zu debuggen, würden dazu führen
RuntimeError: maximum recursion depth exceeded while getting the repr of a Tuple
pprint
und saferepr
schlagen ebenfalls fehl:
...
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
Daher gibt es keine sichere eingebaute Funktion, um dies einzugrenzen: Der folgende Helfer könnte von Nutzen sein:
def excsafe_repr(obj):
try:
return repr(obj)
except:
return object.__repr__(obj).replace('>', ' [exception raised]>')
Und dann gibt es das Problem, dass print
in Python 2 nicht tatsächlich str
repr
verwendet, so dass Sie keine Sicherheit aufgrund fehlender Rekursionsprüfungen haben. Nehmen Sie also den Rückgabewert des Lambda-Monsters oben und Sie können es nicht str
, repr
, aber normale print
(nicht print_function
!) Druckt es schön. Sie können dies jedoch nutzen, um eine SIGSEGV unter Python 2 zu generieren, wenn Sie wissen, dass sie mit der Anweisung print
gedruckt wird:
print eval('(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)')
stürzt Python 2 mit SIGSEGV ab. _/Dies ist WONTFIX im Bug-Tracker . Verwenden Sie daher niemals print
- the-Anweisung, wenn Sie sicher sein möchten. from __future__ import print_function
!
Dies ist kein Absturz, aber
eval('(1,' * 100 + ')' * 100)
wenn ausgeführt, Ausgänge
s_Push: parser stack overflow
Traceback (most recent call last):
File "yyy.py", line 1, in <module>
eval('(1,' * 100 + ')' * 100)
MemoryError
Die MemoryError
kann abgefangen werden, ist eine Unterklasse von Exception
. Der Parser hat einige wirklich konservative Grenzwerte, um Abstürze von Stackoverflows zu vermeiden (Wortspiel beabsichtigt). s_Push: parser stack overflow
wird jedoch durch C-Code an stderr
ausgegeben und kann nicht unterdrückt werden.
Und gestern habe ich gefragt, warum Python 3.4 für einen Absturz von nicht repariert werden kann.
% 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
und Serhiy Storchakas Antwort bestätigte, dass Python-Core-Entwickler SIGSEGV für scheinbar wohlgeformten Code nicht als Sicherheitsproblem betrachten:
Es werden nur Sicherheitsupdates für 3.4 akzeptiert.
Daraus kann gefolgert werden, dass es niemals als sicher angesehen werden kann, Code von Drittanbietern in Python auszuführen, ob bereinigt oder nicht.
Und Nick Coghlan , dann fügte hinzu:
Einige zusätzliche Hintergrundinformationen dazu, warum Segmentierungsfehler, die durch Python-Code hervorgerufen werden, derzeit nicht als Sicherheitsfehler betrachtet werden: Da CPython keine Sicherheits-Sandbox enthält, sind wir bei der Prozessisolation bereits vollständig auf das Betriebssystem angewiesen. Diese Sicherheitsgrenze auf Betriebssystemebene ist nicht davon betroffen, ob der Code "normal" ausgeführt wird oder sich in einem modifizierten Zustand befindet, wenn ein bewusst ausgelöster Segmentierungsfehler vorliegt.
Benutzer können weiterhin DoS durchführen, indem Sie einen Ausdruck eingeben, der eine sehr große Zahl ergibt, wodurch beispielsweise Ihr Arbeitsspeicher gefüllt und der Python-Prozess zum Absturz gebracht wird
'10**10**100'
Ich bin definitiv immer noch neugierig, ob hier eher traditionelle Angriffe möglich sind, etwa die Wiederherstellung von Builtins oder das Erstellen eines Segfault.
BEARBEITEN:
Es stellt sich heraus, dass sogar Pythons Parser dieses Problem hat.
lambda: 10**10**100
wird hängen, weil es versucht, die Konstante vorzuberechnen.
Ich glaube nicht, dass Python auf Sicherheit gegen nicht vertrauenswürdigen Code ausgelegt ist. Mit dem offiziellen Python 2-Interpreter können Sie auf einfache Weise einen Segfault über einen Stack-Overflow (auf dem C-Stack) erzeugen:
eval('()' * 98765)
Von meiner Antwort zur Code-Golf-Frage "Kürzester Code, der SIGSEGV zurückgibt".
Hier ist ein safe_eval-Beispiel, das sicherstellt, dass der bewertete Ausdruck keine unsicheren Token enthält. Es wird nicht versucht, den literal_eval-Ansatz zur Interpretation von AST zu verwenden, sondern die Token-Typen und deren Verwendung auf der Whitelist der echte eval, wenn der Ausdruck den Test bestanden hat.
# 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})
Das Steuern der Wörterbücher locals
und globals
ist äußerst wichtig. Andernfalls könnte jemand einfach eval
oder exec
übergeben und rekursiv aufrufen
safe_eval('''e("""[c for c in ().__class__.__base__.__subclasses__()
if c.__== \'catch_warnings\'][0]()._module.__builtins__""")''',
globals={'e': eval})
Der Ausdruck in der rekursiven eval
ist nur eine Zeichenfolge.
Sie müssen auch die Namen eval
und exec
im globalen Namespace auf einen Wert setzen, der nicht der tatsächlichen eval
oder exec
entspricht. Der globale Namensraum ist wichtig. Wenn Sie einen lokalen Namespace verwenden, wird alles, was einen separaten Namespace erstellt, wie z. B. Verständnis und Lambdas, umgangen
safe_eval('''[eval("""[c for c in ().__class__.__base__.__subclasses__()
if c.__== \'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.__== \'catch_warnings\'][0]()._module.__builtins__"""))()''',
locals={'eval': None})
Auch hier sieht safe_eval
nur eine Zeichenfolge und einen Funktionsaufruf, keine Attributzugriffe.
Sie müssen auch die safe_eval
-Funktion selbst löschen, wenn sie über ein Flag zum Deaktivieren der sicheren Analyse verfügt. Ansonsten könntest du es einfach tun
safe_eval('safe_eval("<dangerous code>", safe=False)')