2014-10-22 15 views
23

Ho un sito Web in cui l'utente immette equazioni matematiche (espressioni) e quindi tali equazioni sono valutate rispetto ai dati (costanti) forniti dal sito Web. Le operazioni matematiche necessarie includono simboli, operazioni aritmetiche, min(), max() e alcune altre funzioni di base. Un'equazione di esempio potrebbe essere:Valutare equazioni matematiche da input utente non sicuro in Python

max(a * b + 100, a/b - 200) 

Si potrebbe semplicemente eval() questo con Python, ma come tutti sappiamo questo porta compromettere il sito. Quale sarebbe l'approccio sicuro per fare la valutazione dell'equazione matematica?

  • motori di analisi e di valutazione Cosa equazione matematica ci sono per Python

  • Se si sceglie di usare Python stesso per valutare l'espressione ci sono delle sandbox Python che limiterebbero il Python, in modo che il fornitore unico utente operatori e funzioni sono disponibili. Python a tutti gli effetti, come la definizione delle funzioni, dovrebbe essere completamente disabilitato. I sottoprocessi sono ok (vedi PyPy sandbox). In particolare, per i loop e altri fori per lo sfruttamento della memoria e l'utilizzo della CPU dovrebbe essere chiuso.

  • Qualsiasi altro approccio, ad es. usando una riga di comando binaria (bc)?

+0

Hai provato a usare google "python safe eval"? Questo produce un numero di risorse altamente rilevanti, incluse alcune su questo sito. – NPE

+0

Naturalmente ho. In effetti ho lavorato con la libreria RestrictedPython in passato: https://pypi.python.org/pypi/RestrictedPython - Sto cercando un po 'più di informazioni su ciò che Google può darti –

+0

La modalità sandbox di PyPy sembra promettente http: // pypy .readthedocs.org/it/latest/sandbox.html –

risposta

7

C'è una relativamente facile di fare questo in Python, senza pacchetti di terze parti.

  • Utilizzando compile() per preparare una linea singola espressione Python per essere bytecode per eval()

  • Non eseguire il bytecode tramite eval(), ma invece eseguirlo nel vostro ciclo codice operativo personalizzato e implementare solo codici operativi che si ha realmente bisogno. Per esempio. nessun built-in, nessun accesso agli attributi, quindi la sandbox non può essere sfuggita.

Tuttavia ci sono alcuni trucchi, come la preparazione per esaurimento CPU e memoria di esaurimento, che non sono specifici di questo metodo e sono problema su altri approcci troppo.

Here is a full blog post about the topic. Here is a related gist. Di seguito viene accorciato il codice di esempio.

"""" 

    The orignal author: Alexer/#python.fi 

""" 

import opcode 
import dis 
import sys 
import multiprocessing 
import time 

# Python 3 required 
assert sys.version_info[0] == 3, "No country for old snakes" 


class UnknownSymbol(Exception): 
    """ There was a function or constant in the expression we don't support. """ 


class BadValue(Exception): 
    """ The user tried to input dangerously big value. """ 

    MAX_ALLOWED_VALUE = 2**63 


class BadCompilingInput(Exception): 
    """ The user tried to input something which might cause compiler to slow down. """ 


def disassemble(co): 
    """ Loop through Python bytecode and match instructions with our internal opcodes. 

    :param co: Python code object 
    """ 
    code = co.co_code 
    n = len(code) 
    i = 0 
    extended_arg = 0 
    result = [] 
    while i < n: 
     op = code[i] 

     curi = i 
     i = i+1 
     if op >= dis.HAVE_ARGUMENT: 
      # Python 2 
      # oparg = ord(code[i]) + ord(code[i+1])*256 + extended_arg 
      oparg = code[i] + code[i+1] * 256 + extended_arg 
      extended_arg = 0 
      i = i+2 
      if op == dis.EXTENDED_ARG: 
       # Python 2 
       #extended_arg = oparg*65536L 
       extended_arg = oparg*65536 
     else: 
      oparg = None 

     # print(opcode.opname[op]) 

     opv = globals()[opcode.opname[op].replace('+', '_')](co, curi, i, op, oparg) 

     result.append(opv) 

    return result 

# For the opcodes see dis.py 
# (Copy-paste) 
# https://docs.python.org/2/library/dis.html 

class Opcode: 
    """ Base class for out internal opcodes. """ 
    args = 0 
    pops = 0 
    pushes = 0 
    def __init__(self, co, i, nexti, op, oparg): 
     self.co = co 
     self.i = i 
     self.nexti = nexti 
     self.op = op 
     self.oparg = oparg 

    def get_pops(self): 
     return self.pops 

    def get_pushes(self): 
     return self.pushes 

    def touch_value(self, stack, frame): 
     assert self.pushes == 0 
     for i in range(self.pops): 
      stack.pop() 


class OpcodeArg(Opcode): 
    args = 1 


class OpcodeConst(OpcodeArg): 
    def get_arg(self): 
     return self.co.co_consts[self.oparg] 


class OpcodeName(OpcodeArg): 
    def get_arg(self): 
     return self.co.co_names[self.oparg] 


class POP_TOP(Opcode): 
    """Removes the top-of-stack (TOS) item.""" 
    pops = 1 
    def touch_value(self, stack, frame): 
     stack.pop() 


class DUP_TOP(Opcode): 
    """Duplicates the reference on top of the stack.""" 
    # XXX: +-1 
    pops = 1 
    pushes = 2 
    def touch_value(self, stack, frame): 
     stack[-1:] = 2 * stack[-1:] 


class ROT_TWO(Opcode): 
    """Swaps the two top-most stack items.""" 
    pops = 2 
    pushes = 2 
    def touch_value(self, stack, frame): 
     stack[-2:] = stack[-2:][::-1] 


class ROT_THREE(Opcode): 
    """Lifts second and third stack item one position up, moves top down to position three.""" 
    pops = 3 
    pushes = 3 
    direct = True 
    def touch_value(self, stack, frame): 
     v3, v2, v1 = stack[-3:] 
     stack[-3:] = [v1, v3, v2] 


class ROT_FOUR(Opcode): 
    """Lifts second, third and forth stack item one position up, moves top down to position four.""" 
    pops = 4 
    pushes = 4 
    direct = True 
    def touch_value(self, stack, frame): 
     v4, v3, v2, v1 = stack[-3:] 
     stack[-3:] = [v1, v4, v3, v2] 


class UNARY(Opcode): 
    """Unary Operations take the top of the stack, apply the operation, and push the result back on the stack.""" 
    pops = 1 
    pushes = 1 


class UNARY_POSITIVE(UNARY): 
    """Implements TOS = +TOS.""" 
    def touch_value(self, stack, frame): 
     stack[-1] = +stack[-1] 


class UNARY_NEGATIVE(UNARY): 
    """Implements TOS = -TOS.""" 
    def touch_value(self, stack, frame): 
     stack[-1] = -stack[-1] 


class BINARY(Opcode): 
    """Binary operations remove the top of the stack (TOS) and the second top-most stack item (TOS1) from the stack. They perform the operation, and put the result back on the stack.""" 
    pops = 2 
    pushes = 1 


class BINARY_POWER(BINARY): 
    """Implements TOS = TOS1 ** TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     print(TOS1, TOS) 
     if abs(TOS1) > BadValue.MAX_ALLOWED_VALUE or abs(TOS) > BadValue.MAX_ALLOWED_VALUE: 
      raise BadValue("The value for exponent was too big") 

     stack[-2:] = [TOS1 ** TOS] 


class BINARY_MULTIPLY(BINARY): 
    """Implements TOS = TOS1 * TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 * TOS] 


class BINARY_DIVIDE(BINARY): 
    """Implements TOS = TOS1/TOS when from __future__ import division is not in effect.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1/TOS] 


class BINARY_MODULO(BINARY): 
    """Implements TOS = TOS1 % TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 % TOS] 


class BINARY_ADD(BINARY): 
    """Implements TOS = TOS1 + TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 + TOS] 


class BINARY_SUBTRACT(BINARY): 
    """Implements TOS = TOS1 - TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 - TOS] 


class BINARY_FLOOR_DIVIDE(BINARY): 
    """Implements TOS = TOS1 // TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 // TOS] 


class BINARY_TRUE_DIVIDE(BINARY): 
    """Implements TOS = TOS1/TOS when from __future__ import division is in effect.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1/TOS] 


class BINARY_LSHIFT(BINARY): 
    """Implements TOS = TOS1 << TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 << TOS] 


class BINARY_RSHIFT(BINARY): 
    """Implements TOS = TOS1 >> TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 >> TOS] 


class BINARY_AND(BINARY): 
    """Implements TOS = TOS1 & TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 & TOS] 


class BINARY_XOR(BINARY): 
    """Implements TOS = TOS1^TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1^TOS] 


class BINARY_OR(BINARY): 
    """Implements TOS = TOS1 | TOS.""" 
    def touch_value(self, stack, frame): 
     TOS1, TOS = stack[-2:] 
     stack[-2:] = [TOS1 | TOS] 


class RETURN_VALUE(Opcode): 
    """Returns with TOS to the caller of the function.""" 
    pops = 1 
    final = True 
    def touch_value(self, stack, frame): 
     value = stack.pop() 
     return value 


class LOAD_CONST(OpcodeConst): 
    """Pushes co_consts[consti] onto the stack.""" # consti 
    pushes = 1 
    def touch_value(self, stack, frame): 
     # XXX moo: Validate type 
     value = self.get_arg() 
     assert isinstance(value, (int, float)) 
     stack.append(value) 


class LOAD_NAME(OpcodeName): 
    """Pushes the value associated with co_names[namei] onto the stack.""" # namei 
    pushes = 1 
    def touch_value(self, stack, frame): 
     # XXX moo: Get name from dict of valid variables/functions 
     name = self.get_arg() 
     if name not in frame: 
      raise UnknownSymbol("Does not know symbol {}".format(name)) 
     stack.append(frame[name]) 


class CALL_FUNCTION(OpcodeArg): 
    """Calls a function. The low byte of argc indicates the number of positional parameters, the high byte the number of keyword parameters. On the stack, the opcode finds the keyword parameters first. For each keyword argument, the value is on top of the key. Below the keyword parameters, the positional parameters are on the stack, with the right-most parameter on top. Below the parameters, the function object to call is on the stack. Pops all function arguments, and the function itself off the stack, and pushes the return value.""" # argc 
    pops = None 
    pushes = 1 

    def get_pops(self): 
     args = self.oparg & 0xff 
     kwargs = (self.oparg >> 8) & 0xff 
     return 1 + args + 2 * kwargs 

    def touch_value(self, stack, frame): 
     argc = self.oparg & 0xff 
     kwargc = (self.oparg >> 8) & 0xff 
     assert kwargc == 0 
     if argc > 0: 
      args = stack[-argc:] 
      stack[:] = stack[:-argc] 
     else: 
      args = [] 
     func = stack.pop() 

     assert func in frame.values(), "Uh-oh somebody injected bad function. This does not happen." 

     result = func(*args) 
     stack.append(result) 


def check_for_pow(expr): 
    """ Python evaluates power operator during the compile time if its on constants. 

    You can do CPU/memory burning attack with ``2**999999999999999999999**9999999999999``. 
    We mainly care about memory now, as we catch timeoutting in any case. 

    We just disable pow and do not care about it. 
    """ 
    if "**" in expr: 
     raise BadCompilingInput("Power operation is not allowed") 


def _safe_eval(expr, functions_and_constants={}, check_compiling_input=True): 
    """ Evaluate a Pythonic math expression and return the output as a string. 

    The expr is limited to 1024 characters/1024 operations 
    to prevent CPU burning or memory stealing. 

    :param functions_and_constants: Supplied "built-in" data for evaluation 
    """ 

    # Some safety checks 
    assert len(expr) < 1024 

    # Check for potential bad compiler input 
    if check_compiling_input: 
     check_for_pow(expr) 

    # Compile Python source code to Python code for eval() 
    code = compile(expr, '', 'eval') 

    # Dissect bytecode back to Python opcodes 
    ops = disassemble(code) 
    assert len(ops) < 1024 

    stack = [] 
    for op in ops: 
     value = op.touch_value(stack, functions_and_constants) 

    return value 
5

Disclaimer: Sono l'Alexer menzionato nel codice nell'altra risposta. Per essere onesti, ho suggerito l'approccio del parsing del bytecode solo a metà scherzoso, dal momento che mi è capitato di avere il 99% del codice in giro per un progetto non correlato e quindi di poter montare un POC in un paio di minuti. Detto questo, non dovrebbe esserci nulla di sbagliato in esso, di per sé; è solo che è un macchinario più complesso che è necessario per questo compito. Infatti, è essere in grado di farla franca semplicemente smontando il codice [controllando gli opcode contro una whitelist], controllando che le costanti e i nomi siano validi, e eseguendoli con una valutazione semplice e malvagia dopo di ciò. Dovresti solo perdere la possibilità di inserire controlli extra paranoici durante l'esecuzione. (Un altro disclaimer: non mi sentirei abbastanza a mio agio con eval)

In ogni caso, ho avuto un momento noioso, quindi ho scritto del codice per farlo in modo intelligente; usando AST invece del bytecode. È solo una bandiera aggiuntiva a compile().(O semplicemente ast.parse(), dal momento che si vorrà i tipi dal modulo comunque)

import ast 
import operator 

_operations = { 
     ast.Add: operator.add, 
     ast.Sub: operator.sub, 
     ast.Mult: operator.mul, 
     ast.Div: operator.div, 
     ast.Pow: operator.pow, 
} 

def _safe_eval(node, variables, functions): 
     if isinstance(node, ast.Num): 
       return node.n 
     elif isinstance(node, ast.Name): 
       return variables[node.id] # KeyError -> Unsafe variable 
     elif isinstance(node, ast.BinOp): 
       op = _operations[node.op.__class__] # KeyError -> Unsafe operation 
       left = _safe_eval(node.left, variables, functions) 
       right = _safe_eval(node.right, variables, functions) 
       if isinstance(node.op, ast.Pow): 
         assert right < 100 
       return op(left, right) 
     elif isinstance(node, ast.Call): 
       assert not node.keywords and not node.starargs and not node.kwargs 
       assert isinstance(node.func, ast.Name), 'Unsafe function derivation' 
       func = functions[node.func.id] # KeyError -> Unsafe function 
       args = [_safe_eval(arg, variables, functions) for arg in node.args] 
       return func(*args) 

     assert False, 'Unsafe operation' 

def safe_eval(expr, variables={}, functions={}): 
     node = ast.parse(expr, '<string>', 'eval').body 
     return _safe_eval(node, variables, functions) 

if __name__ == '__main__': 
     import math 

     print safe_eval('sin(a*pi/b)', dict(a=1, b=2, pi=math.pi), dict(sin=math.sin)) 

La stessa cosa vale per questo come per la versione bytecode; se controlli le operazioni contro una whitelist e controlli che i nomi e i valori siano validi, dovresti essere in grado di cavartela con eval chiamando su AST. (Ma ancora una volta, non lo farei mai. Perché paranoico e paranoia va bene quando si tratta di eval)

Problemi correlati