web-dev-qa-db-de.com

Python eval: Ist es immer noch gefährlich, Builtins und Attributzugriffe zu deaktivieren?

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).

31
asmeurer

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      '' 
21
vaultah

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 printed (auf Python 3), stred oder repred 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 strrepr 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.

17
Antti Haapala

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. 

10
asmeurer

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".

6
feersum

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})
0
tardyp

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)')
0
asmeurer