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