Python eval: est-ce encore dangereux si je désactive les builtins et l'accès aux attributs?

nous savons tous que eval est dangereux , même si vous cachez des fonctions dangereuses, parce que vous pouvez utiliser les fonctionnalités d'introspection de Python pour creuser dans les choses et les ré-extraire. Par exemple, même si vous supprimez __builtins__ , vous pouvez les récupérer avec

[c for c in ().__class__.__base__.__subclasses__()  
 if c.__name__ == 'catch_warnings'][0]()._module.__builtins__

Cependant, chaque exemple que j'ai vu de ceci utilise l'accès d'attribut. Que faire si je désactive tous les builtins, et désactiver l'accès aux attributs (par la segmentation de l'entrée avec un Python tokenizer et de la rejeter si elle a un attribut jeton d'accès)?

et avant que vous ne me le demandiez, non, pour mon étui, Je n'ai pas besoin de l'un ou l'autre, donc il n'est pas trop paralysant.

ce que j'essaie de faire, c'est de rendre plus sûre la fonction sympify de SymPy. Actuellement, il tokenizes l'entrée, n'certaines transformations sur elle, et est évaluée comme dans un espace de noms. Mais c'est dangereux parce qu'il permet l'accès aux attributs (même si ça n'a pas vraiment besoin).

30
demandé sur Fermi paradox 2016-03-04 22:55:17

6 réponses

je vais mentionner une des nouvelles fonctionnalités de Python 3.6 - F-strings .

ils peuvent évaluer des expressions,

>>> eval('f"{().__class__.__base__}"', {'__builtins__': None}, {})
"<class 'object'>"

mais l'accès à l'attribut ne sera pas détecté par le tokenizer de 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      '' 
21
répondu vaultah 2016-03-04 21:07:19

il est possible de construire une valeur de retour de eval qui lancerait un exception extérieur eval si vous avez essayé de print , log , repr , anything:

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

cela crée un tuple imbriqué de la forme (1,(1,(1,(1... ; cette valeur ne peut pas être print ed (sur Python 3), str ed ou repr ed; toute tentative de débogage conduirait à

RuntimeError: maximum recursion depth exceeded while getting the repr of a tuple

pprint et saferepr ne tient pas trop:

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

il n'y a donc pas de fonction intégrée sûre pour stringifier ceci: l'aide suivante pourrait être utile:

def excsafe_repr(obj):
    try:
        return repr(obj)
    except:
        return object.__repr__(obj).replace('>', ' [exception raised]>')

et puis il y a le problème que print en Python 2 n'utilise pas réellement str / repr , donc vous n'avez pas de sécurité en raison de absence de contrôles de récursion. C'est-à-dire, prendre la valeur de retour du monstre lambda ci-dessus, et vous ne pouvez pas str , repr il, mais ordinaire print (pas print_function !) imprime bien. Cependant, vous pouvez exploiter ceci pour générer un SIGSEGV sur Python 2 Si vous savez qu'il sera imprimé en utilisant la déclaration print :

print eval('(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)')

incidents Python 2 avec un SIGSEGV . C'est WONTFIX en le bug tracker . Ainsi, n'utilisez jamais print - la-déclaration si vous voulez être sûr. from __future__ import print_function !


ce n'est pas un accident, mais

eval('(1,' * 100 + ')' * 100)

lorsque vous exécutez, sorties

s_push: parser stack overflow
Traceback (most recent call last):
  File "yyy.py", line 1, in <module>
    eval('(1,' * 100 + ')' * 100)
MemoryError

le MemoryError peut être capturé, est une sous-classe de Exception . L'analyseur a quelques vraiment limites conservatrices pour éviter les accidents de flux de stockage (jeu de mots prévu). Cependant, s_push: parser stack overflow est produit en stderr par le code C, et ne peut pas être supprimé.


et hier j'ai demandé pourquoi Python 3.4 n'est pas fixé pour un crash de ,

% 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

et la réponse de Serhiy Storchaka's a confirmé que les devs Python core ne considèrent pas SIGSEGV sur le code apparemment bien formé un problème de sécurité:

seuls les correctifs de sécurité sont acceptés pour 3.4.

ainsi, il peut être conclu qu'il ne peut jamais être considéré comme sûr d'exécuter n'importe quel code de tiers en Python, aseptisé ou non.

et Nick Coghlan puis a ajouté :

et comme arrière-plan supplémentaire pour expliquer pourquoi les défauts de segmentation provoqués par le code Python ne sont pas actuellement considéré comme un bug de sécurité: puisque CPython ne comprend pas de bac à sable de sécurité, nous comptons déjà entièrement sur L'OS pour fournir l'isolation des processus. Cette limite de sécurité au niveau de L'OS n'est pas affectée par le fait que le code tourne "normalement", ou dans un état modifié suite à une défaillance de segmentation délibérément déclenchée.

17
répondu Antti Haapala 2017-05-23 10:31:33

les utilisateurs peuvent toujours vous DoS en saisissant une expression qui évalue à un nombre énorme, qui remplirait votre mémoire et écraserait le processus Python, par exemple

'10**10**100'

je suis certainement toujours curieux si des attaques plus traditionnelles, comme récupérer des builtins ou créer un segfault, sont possibles ici.

EDIT:

il s'avère que même L'analyseur de Python a ce problème.

lambda: 10**10**100

sera accrocher, parce qu'il essaie de précalculer la constante.

10
répondu asmeurer 2016-08-03 18:49:18

Je ne crois pas que Python soit conçu pour avoir une sécurité contre du code non fiable. Voici un moyen facile d'induire un segfault via un débordement de pile (sur la pile C) dans L'interpréteur officiel de Python 2:

eval('()' * 98765)

De mon réponse les "le plus court de code qui renvoie SIGSEGV" Code de Golf de la question.

6
répondu feersum 2017-04-13 12:38:59

contrôler les dictionnaires locals et globals est extrêmement important. Sinon, quelqu'un pourrait simplement passer dans eval ou exec , et l'appeler récursivement

safe_eval('''e("""[c for c in ().__class__.__base__.__subclasses__() 
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""")''', 
    globals={'e': eval})

l'expression dans la récursive eval n'est qu'une chaîne.

vous devez également définir les noms eval et exec dans l'espace Nam global à quelque chose qui n'est pas le vrai eval ou exec . Global namespace est important. Si vous utilisez un namespace local, tout ce qui crée un namespace séparé, comme des compréhensions et lambdas, fonctionnera autour de lui

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

encore une fois, ici, safe_eval ne voit qu'une chaîne et un appel de fonction, pas d'accès d'attribut.

vous devez également effacer la fonction safe_eval elle-même, si elle a un drapeau pour désactiver l'analyse Sécuritaire. Sinon vous pourriez simplement faire

safe_eval('safe_eval("<dangerous code>", safe=False)')
0
répondu asmeurer 2017-04-17 23:35:47

voici un exemple safe_eval qui garantira que l'expression évaluée ne contient pas de tokens dangereux. Il ne cherche pas à prendre l'approche littéral_eval d'interprétation de L'AST mais plutôt whitelist les types token et utiliser la vraie eval si l'expression a passé le test.

# 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
répondu tardyp 2018-01-07 09:11:55