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).
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 ''
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 print
ed (sur Python 3), str
ed ou repr
ed; 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 str
repr
, 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.
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.
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.
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})
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)')