web-dev-qa-db-fra.com

Python eval: est-il toujours dangereux de désactiver les composants intégrés et d'attribuer l'accès?

Nous savons tous que eval est dangereux , même si vous masquez des fonctions dangereuses, car vous pouvez utiliser les fonctions d’introspection de Python pour creuser et ré-extraire des éléments. Par exemple, même si vous supprimez __builtins__, vous pouvez les récupérer avec 

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

Cependant, tous les exemples que j'ai vus utilisent un accès par attribut. Que se passe-t-il si je désactive toutes les fonctions intégrées, et désactive l'accès aux attributs (en créant une entrée symbolique avec un générateur de jetons Python et en la rejetant si elle contient un jeton d'accès aux attributs)? 

Et avant que vous ne demandiez, non, pour mon cas d'utilisation, je n'ai besoin d'aucun de ceux-ci, donc ce n'est pas trop invalidant. 

Ce que j'essaie de faire est de rendre la fonction sympify de SymPy plus sûre. Actuellement, il tokenize l'entrée, y effectue quelques transformations et l'évalue dans un espace de noms. Mais c'est dangereux car cela permet l'accès aux attributs (même s'il n'en a vraiment pas besoin).

31
asmeurer

Je vais mentionner l'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 aux attributs 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
vaultah

Il est possible de construire une valeur de retour à partir de eval qui enverrait un exceptionextérieureval si vous tentiez de print, log, repr, n'importe quoi:

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 printed (sur Python 3), stred ou repred; toutes les tentatives de déboguer conduiraient à 

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

pprint et saferepr échouent aussi:

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

Par conséquent, il n’existe pas de fonction intégrée sécurisée pour renforcer cette fonction: 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 qui veut que print en Python 2 n'utilise pas réellement strrepr, de sorte que vous n'avez aucune sécurité en l'absence de contrôles de récursivité. Autrement dit, prenez la valeur de retour du monstre lambda ci-dessus, et vous ne pouvez pas le remplacer par str, repr, mais print (et non print_function!) Ordinaire l’affiche correctement. Cependant, vous pouvez exploiter ceci pour générer un fichier SIGSEGV sur Python 2 si vous savez qu'il sera imprimé à l'aide de l'instruction print:

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

_/bloque Python 2 avec SIGSEGV. Ceci est WONTFIX dans le suivi des bogues . Ainsi, n'utilisez jamais print-the-statement si vous voulez être en sécurité. from __future__ import print_function!


Ce n'est pas un crash, mais

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

lors de l'exécution, les sorties

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

MemoryError peut être attrapé, est une sous-classe de Exception. L'analyseur a quelques limites vraiment conservatrices pour éviter les plantages de stackoverflows (jeu de mots voulu). Cependant, s_Push: parser stack overflow est généré dans stderr par le code C et ne peut pas être supprimé.


Et hier encore, j'ai demandé pourquoi Python 3.4 ne serait-il pas corrigé pour un plantage 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 a confirmé que les développeurs Python ne considèrent pas SIGSEGV sur du code apparemment bien formé comme un problème de sécurité:

Seuls les correctifs de sécurité sont acceptés pour la version 3.4.

On peut donc en conclure qu'il ne peut jamais être considéré comme sûr d'exécuter un code provenant d'une tierce partie en Python, désinfecté ou non.

Et Nick Coghlan then ajouté :

Et pour expliquer en quoi les erreurs de segmentation provoquées par le code Python ne sont pas considérées actuellement comme un bogue de sécurité: puisque CPython n’inclut pas de sandbox de sécurité, nous comptons déjà entièrement sur le système d’exploitation pour assurer l’isolation des processus . Cette limite de sécurité au niveau du système d'exploitation n'est pas affectée par le fait que le code s'exécute "normalement" ou dans un état modifié à la suite d'une erreur de segmentation déclenchée délibérément.

17
Antti Haapala

Les utilisateurs peuvent quand même faire votre DoS en entrant une expression dont le nombre est élevé, ce qui remplirait votre mémoire et ferait planter le processus Python, par exemple

'10**10**100'

Je suis certainement toujours curieux de savoir si des attaques plus traditionnelles, telles que la récupération d'éléments intégrés ou la création d'une erreur de segmentation, sont possibles ici. 

MODIFIER: 

Il s'avère que même l’analyseur de Python a ce problème. 

lambda: 10**10**100

va se bloquer, car il tente de précalculer la constante. 

10
asmeurer

Je ne pense pas que Python soit conçu pour avoir une sécurité contre le code non fiable. Voici un moyen facile d’induire une erreur de segmentation via un débordement de pile (sur la pile C) dans l’interpréteur officiel de Python 2:

eval('()' * 98765)

De mon répondre au "Code le plus court qui retourne SIGSEGV" Code Golf question.

6
feersum

Voici un exemple safe_eval qui garantit que l'expression évaluée ne contient pas de jetons dangereux. Elle n'essaie pas d'adopter l'approche literal_eval consistant à interpréter le AST, mais plutôt de mettre les types de jetons dans la liste blanche et d'utiliser la valeur réelle si expression réussi 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
tardyp

Contrôler les dictionnaires locals et globals est extrêmement important. Sinon, quelqu'un pourrait simplement passer eval ou exec et l'appeler de manière récursive

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

L'expression dans la variable eval récursive est simplement une chaîne. 

Vous devez également définir les noms eval et exec dans l'espace de noms global sur quelque chose qui n'est pas réel eval ou exec. L'espace de noms global est important. Si vous utilisez un espace de noms local, tout ce qui crée un espace de noms séparé, tel que compréhensions et lambdas, va le contourner.

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

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

Vous devez également effacer la fonction safe_eval elle-même, si elle a un indicateur pour désactiver l'analyse sécurisée. Sinon, vous pourriez simplement faire

safe_eval('safe_eval("<dangerous code>", safe=False)')
0
asmeurer