2016-03-04 10 views
27

Sappiamo tutti che eval is dangerous, anche se si nascondono funzioni pericolose, perché è possibile utilizzare le funzionalità di introspezione di Python per scavare nelle cose e riestrarle. Ad esempio, anche se si elimina __builtins__, è possibile recuperarli conEval Python: è ancora pericoloso se disabilito i build e l'accesso agli attributi?

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

Tuttavia, tutti gli esempi che ho visto di questo utilizza attributo di accesso. Cosa succede se disabilito tutti i builtin, e disabilitare l'accesso agli attributi (con la tokenizzazione dell'input con un tokenizzatore Python e rifiutandolo se ha un token di accesso all'attributo)?

E prima che tu chieda, no, per il mio caso d'uso, non ho bisogno di nessuno di questi, quindi non è troppo paralizzante.

Quello che sto cercando di fare è rendere la funzione di SymPy sympify più sicura. Attualmente concede l'input, esegue alcune trasformazioni e lo elabora in uno spazio dei nomi. Ma non è sicuro perché consente l'accesso agli attributi (anche se in realtà non ne ha bisogno).

+7

Questo dipende da cosa intendi per pericoloso ... Immagino che un utente malintenzionato potrebbe creare un'espressione per fare un _really_ grande numero intero che li induca a corto di memoria .... – mgilson

+2

@mgilson che è un punto valido Suppongo che sia possibile proteggersi da questo mettendo protezioni di memoria/tempo sulla vostra applicazione, ma sicuramente ne vale la pena essere a conoscenza. – asmeurer

+8

Penso che questo dipenda anche dalla gente del posto che passi in ... 'a + b' è sicuro solo come' a .__ add__' e 'b .__ radd__' sono sicuri ... – mgilson

risposta

9

Gli utenti possono comunque si DoS inserendo un'espressione che restituisce un numero enorme, che avrebbe riempito la tua memoria e crash del processo di Python, per esempio

'10**10**100' 

Sono sicuramente ancora curioso di sapere se gli attacchi più tradizionali, come recuperare i builtin o creare un segfault, qui sono possibili.

EDIT:

Si scopre, parser anche di Python ha questo problema.

lambda: 10**10**100 

si bloccherà perché tenta di precompilare la costante.

+0

L'unico modo per evitare ciò è utilizzare un timeout che blocca l'esecuzione del thread che viene eseguito dopo x time o quando vengono eseguite troppe allocazioni (il che potrebbe essere piuttosto difficile da fare ...) – Bakuriu

+1

@Bakuriu: Se stai lavorando in Python, sarà molto più difficile perché è probabile da valutare tenendo premuto il GIL. Per un numero così grande, c'è anche una possibilità diversa da OOMing, a seconda delle circostanze. – Kevin

16

E 'possibile costruire un valore restituito da eval che getterebbe una un'eccezionefuorieval se si è tentato di print, log, repr, qualsiasi 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))''') 

Questo crea una tupla nidificato di modulo (1,(1,(1,(1...; tale valore non può essere print ed (su Python 3), str edo repr ed; tutti i tentativi di eseguire il debug porterebbe a

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

pprint e saferepr non troppo:

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

Quindi non v'è alcuna funzione di sicurezza integrata per stringa i questo: le seguenti aiuto potrebbe essere utile:

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

E poi c'è il problema che print in Python in realtà non utilizza str/repr, quindi non si ha alcuna sicurezza a causa della mancanza di controlli di ricorsione.Cioè, prendi il valore di ritorno del mostro lambda sopra, e non puoi str, repr, ma il numero ordinario print (non print_function!) Lo stampa bene. Tuttavia, è possibile sfruttare questo per generare un SIGSEGV su Python 2 se si sa che verrà stampato utilizzando l'istruzione print:

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

crash Python 2 con SIGSEGV. This is WONTFIX in the bug tracker. Quindi non usare mai print -la dichiarazione se vuoi essere sicuro. from __future__ import print_function!


Questa non è una dura, ma

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

quando viene eseguito, uscite

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

Il MemoryError possono essere catturati, è una sottoclasse di Exception. Il parser ha qualche really conservative limits to avoid crashes from stackoverflows (gioco di parole). Tuttavia, s_push: parser stack overflow viene emesso a stderr dal codice C e non può essere soppresso.


E proprio ieri ho chiesto why doesn't Python 3.4 be fixed for a crash from,

% 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 

e Serhiy Storchaka's answer confermato che gli sviluppatori di base Python non considerano SIGSEGV sul codice apparentemente ben formato un problema di sicurezza:

solo la sicurezza le correzioni sono accettate per 3.4.

Quindi si può concludere che non può mai essere considerato sicuro eseguire qualsiasi codice da terze parti in Python, disinfettato o meno.

E Nick Coghlan poi added:

E come alcuni retroscena aggiuntive sul motivo per cui gli errori di segmentazione provocate dal codice Python non sono attualmente considerati un bug di sicurezza: dal CPython non include una sandbox di sicurezza, noi' stiamo già facendo affidamento interamente sul sistema operativo per fornire l'isolamento del processo. Questo limite di sicurezza a livello di sistema operativo non è influenzato dal fatto che il codice sia in esecuzione "normalmente" o in uno stato modificato in seguito a un errore di segmentazione deliberatamente attivato.

+0

"* Quindi non esiste un modo sicuro per scaricare questo valore nei registri, o qualsiasi altra cosa - qualsiasi tentativo comporterebbe il lancio di ulteriori eccezioni. *" Degno di bug? – cat

+0

Un problema ben noto. –

+2

Vedi, Haskell non ha questo problema :-D Anche la più strana delle cose può uscire e può essere facilmente catturata o stringa in una stringa infinitamente lunga, che è possibile stampare una parte arbitrariamente lunga di. –

19

Ho intenzione di citare una delle nuove funzionalità di Python 3.6 - f-strings.

Essi possono valutare le espressioni,

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

ma l'accesso attributo non verrà rilevato dal tokenizer di 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  '' 
+1

Bene, devi semplicemente considerare il contenuto di tutte le stringhe di f e controllarle (o più in sicurezza: non permetterle). – Bakuriu

+16

Questo evidenzia in realtà quanto un obiettivo in movimento che cerca di proteggere 'eval' è. In questo momento, sono le corde. Chi sa cosa porterà 3.7? – user2357112

6

Non credo Python è progettato per avere qualsiasi protezione contro non attendibile codice.Ecco un modo semplice per indurre un segfault tramite overflow dello stack (sullo stack C) nella Python 2 interprete ufficiale:

eval('()' * 98765) 

Dal mio answer al "codice più breve che restituisce SIGSEGV" Codice Golf domanda.

+0

In Python 3 si ottiene la massima profondità di ricorsione superata. Se il tuo Python 2 non ha dato un'eccezione o crash, è necessario aumentare il numero! Avevo bisogno di 987650 su 1 sistema. –

+0

Il tuo originale si è schiantato lì btw: D –

0

Controllare i dizionari locals e globals è estremamente importante. In caso contrario, qualcuno potrebbe semplicemente passare eval o exec, e lo chiamano in modo ricorsivo

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

L'espressione nella ricorsivo eval è solo una stringa.

È inoltre necessario impostare i nomi eval e exec nel namespace globale a qualcosa che non è il vero eval o exec. Lo spazio dei nomi globale è importante. Se si utilizza un namespace locale, tutto ciò che crea uno spazio dei nomi separato, come comprensioni e lambda, lavorerà intorno ad esso

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

Ancora una volta, qui, safe_eval vede solo una stringa e una chiamata di funzione, non attribuire accessi.

È inoltre necessario cancellare la funzione safe_eval, se dispone di un flag per disabilitare l'analisi sicura. In caso contrario, si potrebbe semplicemente fare

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

Ecco un esempio safe_eval, che farà in modo che l'espressione valutata non contengono i token non sicuri. Non tenta di prendere l'approccio literal_eval di interpretare l'AST ma piuttosto di inserire nella whitelist i tipi di token e utilizzare l'eval reale se l'espressione ha superato il 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

Funziona con gli oggetti Django e i suoi metodi? – bkmagnetron

Problemi correlati