example - Evaluación de Python: ¿sigue siendo peligroso si deshabilito los accesos integrados y los atributos?
python exec (6)
Aquí hay un ejemplo de safe_eval que asegurará que la expresión evaluada no contenga tokens no seguros. No intenta adoptar el enfoque literal de la interpretación del AST, sino que hace una lista blanca de los tipos de token y usa la evaluación real si la expresión pasa la prueba.
# 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})
Todos sabemos que eval
es peligroso , incluso si ocultas funciones peligrosas, porque puedes usar las características de introspección de Python para profundizar en las cosas y volver a extraerlas. Por ejemplo, incluso si elimina __builtins__
, puede recuperarlos con
[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == ''catch_warnings''][0]()._module.__builtins__
Sin embargo, todos los ejemplos que he visto de esto usan acceso a atributos. ¿Qué sucede si deshabilito todos los elementos incorporados y deshabilito el acceso a los atributos (al crear un token de la entrada con un tokenizador de Python y rechazarla si tiene un token de acceso al atributo)?
Y antes de preguntar, no, para mi caso de uso, no necesito ninguno de estos, por lo que no es demasiado paralizante.
Lo que estoy tratando de hacer es hacer que la función sympify de sympify más segura. Actualmente tokeniza la entrada, realiza algunas transformaciones en ella y la guarda en un espacio de nombres. Pero no es seguro porque permite el acceso a los atributos (aunque en realidad no lo necesita).
Controlar los diccionarios locals
y globals
es extremadamente importante. De lo contrario, alguien podría aprobar eval
o exec
y llamarlo de forma recursiva.
safe_eval(''''''e("""[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == /'catch_warnings/'][0]()._module.__builtins__""")'''''',
globals={''e'': eval})
La expresión en la eval
recursiva es solo una cadena.
También debe configurar los nombres de eval
y exec
en el espacio de nombres global a algo que no sea real eval
o exec
. El espacio de nombres global es importante. Si usa un espacio de nombres local, cualquier cosa que cree un espacio de nombres separado, como las comprensiones y las lambdas, funcionará a su alrededor.
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})
Nuevamente, aquí, safe_eval
solo ve una cadena y una llamada de función, no atributos de acceso.
También debe borrar la función safe_eval
, si tiene un indicador para deshabilitar el análisis seguro. De lo contrario simplemente podrías hacer
safe_eval(''safe_eval("<dangerous code>", safe=False)'')
Es posible construir un valor de retorno desde eval
que arrojaría una excepción fuera de eval
si intentara print
, log
, repr
cualquier cosa:
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))'''''')
Esto crea una tupla de forma anidada (1,(1,(1,(1...
; ese valor no puede print
(en Python 3), str
o repr
; todos los intentos de depuración llevaría a
RuntimeError: maximum recursion depth exceeded while getting the repr of a tuple
pprint
y saferepr
fallan también:
...
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
Por lo tanto, no hay una función incorporada segura para esta cadena: el siguiente asistente podría ser de utilidad:
def excsafe_repr(obj):
try:
return repr(obj)
except:
return object.__repr__(obj).replace(''>'', '' [exception raised]>'')
Y luego está el problema de que print
en Python 2 en realidad no usa str
/ repr
, por lo que no tiene ninguna seguridad debido a la falta de controles de recursión. Es decir, tome el valor de retorno del monstruo lambda de arriba, y no puede print_function
, reproducirlo, pero la print
normal (no print_function
!) Lo imprime muy bien. Sin embargo, puede explotar esto para generar un SIGSEGV en Python 2 si sabe que se imprimirá utilizando la declaración de print
:
print eval(''(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)'')
se estrella Python 2 con SIGSEGV . Esto es WONTFIX en el rastreador de errores . Por lo tanto, nunca use print
-the-statement si quiere estar seguro. from __future__ import print_function
!
Esto no es un choque, pero
eval(''(1,'' * 100 + '')'' * 100)
cuando se ejecuta, salidas
s_push: parser
Traceback (most recent call last):
File "yyy.py", line 1, in <module>
eval(''(1,'' * 100 + '')'' * 100)
MemoryError
El MemoryError
puede ser capturado, es una subclase de Exception
. El analizador tiene algunos límites realmente conservadores para evitar bloqueos de los flujos de apilamiento (juego de palabras destinado). Sin embargo, el s_push: parser
a stderr
mediante un código C y no se puede suprimir.
Y ayer pregunté por qué Python 3.4 no se arregla para un choque desde
% 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
y la respuesta de Serhiy Storchaka confirmó que los desarrolladores de Python Core no consideran a SIGSEGV en un código aparentemente bien formado como un problema de seguridad:
Solo se aceptan correcciones de seguridad para 3.4.
Por lo tanto, se puede concluir que nunca se puede considerar seguro ejecutar cualquier código de terceros en Python, ya sea saneado o no.
Y Nick Coghlan luego added :
Y como algunos antecedentes adicionales de por qué las fallas de segmentación provocadas por el código de Python no se consideran actualmente un error de seguridad: como CPython no incluye un recinto de seguridad, ya dependemos completamente del sistema operativo para proporcionar aislamiento del proceso. El límite de seguridad a nivel del sistema operativo no se ve afectado por si el código se ejecuta "normalmente" o en un estado modificado luego de un fallo de segmentación activado deliberadamente.
Los usuarios pueden seguir haciéndolo ingresando una expresión que se evalúa en un número enorme, que llenaría su memoria y colapsaría el proceso de Python, por ejemplo
''10**10**100''
Definitivamente sigo sintiendo curiosidad por los ataques más tradicionales, como la recuperación de incorporaciones o la creación de una falla de seguridad, aquí es posible.
EDITAR:
Resulta que incluso el analizador de Python tiene este problema.
lambda: 10**10**100
Se cuelga, porque trata de calcular previamente la constante.
No creo que Python esté diseñado para tener seguridad contra códigos no confiables. Aquí hay una manera fácil de inducir un segfault a través del desbordamiento de pila (en la pila C) en el intérprete oficial de Python 2:
eval(''()'' * 98765)
Desde mi answer al "Código más corto que devuelve SIGSEGV", pregunta sobre el Código de Golf.
Voy a mencionar una de las nuevas características de Python 3.6 - f-strings .
Pueden evaluar expresiones,
>>> eval(''f"{().__class__.__base__}"'', {''__builtins__'': None}, {})
"<class ''object''>"
pero el atributo de acceso no será detectado por el tokenizador 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 ''''