Skip to content

Come posso accettare ed eseguire il codice dell'utente in modo sicuro sulla mia applicazione web?

Dopo aver consultato specialisti in questa materia, programmatori di diverse aree e insegnanti, abbiamo trovato la soluzione al dilemma e la lasciamo riflessa in questo post.

Soluzione:

È una domanda importante. In python il sandboxing non è banale.

È uno dei pochi casi in cui ci si chiede quale versione dell'interprete python si stia utilizzando. Ad esempio, Jyton genera bytecode Java e la JVM ha il suo meccanismo per eseguire il codice in modo sicuro.

Per CPython, l'interprete predefinito, in origine c'erano stati alcuni tentativi di creare una modalità di esecuzione ristretta, che sono stati abbandonati molto tempo fa.

Attualmente esiste un progetto non ufficiale, RestrictedPython, che potrebbe fornire ciò di cui si ha bisogno. È non è una sandbox completa, cioè non vi darà accesso limitato al filesystem o altro, ma per le vostre esigenze potrebbe essere sufficiente.

Fondamentalmente i ragazzi hanno riscritto la compilazione di python in modo più limitato.

Ciò che permette di fare è compilare un pezzo di codice e poi eseguirlo, il tutto in una modalità ristretta. Per esempio:

from RestrictedPython import safe_builtins, compile_restricted

source_code = """
print('Hello world, but secure')
"""

byte_code = compile_restricted(
    source_code,
    filename='',
    mode='exec'
)
exec(byte_code, {__builtins__ = safe_builtins})

>>> Hello world, but secure

Esecuzione con builtins = safe_builtins disabilita la funzione pericolosi come aprire un file, importare o altro. Esistono anche altre varianti di builtin e altre opzioni; prendetevi un po' di tempo per leggere la documentazione, è piuttosto buona.

MODIFICA:

Ecco un esempio per il vostro caso d'uso

from RestrictedPython import safe_builtins, compile_restricted
from RestrictedPython.Eval import default_guarded_getitem

def execute_user_code(user_code, user_func, *args, **kwargs):
    """ Executed user code in restricted env
        Args:
            user_code(str) - String containing the unsafe code
            user_func(str) - Function inside user_code to execute and return value
            *args, **kwargs - arguments passed to the user function
        Return:
            Return value of the user_func
    """

    def _apply(f, *a, **kw):
        return f(*a, **kw)

    try:
        # This is the variables we allow user code to see. @result will contain return value.
        restricted_locals = {
            "result": None,
            "args": args,
            "kwargs": kwargs,
        }

        # If you want the user to be able to use some of your functions inside his code,
        # you should add this function to this dictionary.
        # By default many standard actions are disabled. Here I add _apply_ to be able to access
        # args and kwargs and _getitem_ to be able to use arrays. Just think before you add
        # something else. I am not saying you shouldn't do it. You should understand what you
        # are doing thats all.
        restricted_globals = {
            "__builtins__": safe_builtins,
            "_getitem_": default_guarded_getitem,
            "_apply_": _apply,
        }

        # Add another line to user code that executes @user_func
        user_code += "nresult = {0}(*args, **kwargs)".format(user_func)

        # Compile the user code
        byte_code = compile_restricted(user_code, filename="", mode="exec")

        # Run it
        exec(byte_code, restricted_globals, restricted_locals)

        # User code has modified result inside restricted_locals. Return it.
        return restricted_locals["result"]

    except SyntaxError as e:
        # Do whaever you want if the user has code that does not compile
        raise
    except Exception as e:
        # The code did something that is not allowed. Add some nasty punishment to the user here.
        raise

Ora avete una funzione execute_user_codeche riceve del codice non sicuro come stringa, il nome di una funzione da questo codice, degli argomenti e restituisce il valore di ritorno della funzione con gli argomenti dati.

Ecco un esempio molto stupido di codice utente:

example = """
def test(x, name="Johny"):
    return name + " likes " + str(x*x)
"""
# Lets see how this works
print(execute_user_code(example, "test", 5))
# Result: Johny likes 25

Ma ecco cosa succede quando il codice utente cerca di fare qualcosa di non sicuro:

malicious_example = """
import sys
print("Now I have the access to your system, muhahahaha")
"""
# Lets see how this works
print(execute_user_code(malicious_example, "test", 5))
# Result - evil plan failed:
#    Traceback (most recent call last):
#  File "restr.py", line 69, in 
#    print(execute_user_code(malitious_example, "test", 5))
#  File "restr.py", line 45, in execute_user_code
#    exec(byte_code, restricted_globals, restricted_locals)
#  File "", line 2, in 
#ImportError: __import__ not found

Possibile estensione:

Si noti che il codice utente viene compilato a ogni chiamata alla funzione. Tuttavia, è possibile che si voglia compilare il codice utente una volta e poi eseguirlo con parametri diversi. Quindi è sufficiente salvare il file byte_code da qualche parte, quindi richiamare exec con un diverso insieme di parametri restricted_locals ogni volta.

EDIT2:

Se si vuole usare l'importazione, si può scrivere una propria funzione di importazione che permetta di usare solo i moduli che si considerano sicuri. Esempio:

def _import(name, globals=None, locals=None, fromlist=(), level=0):
    safe_modules = ["math"]
    if name in safe_modules:
       globals[name] = __import__(name, globals, locals, fromlist, level)
    else:
        raise Exception("Don't you even think about it {0}".format(name))

safe_builtins['__import__'] = _import # Must be a part of builtins
restricted_globals = {
    "__builtins__": safe_builtins,
    "_getitem_": default_guarded_getitem,
    "_apply_": _apply,
}

....
i_example = """
import math
def myceil(x):
    return math.ceil(x)
"""
print(execute_user_code(i_example, "myceil", 1.5))

Si noti che questa funzione di importazione di esempio è MOLTO primitiva, non funzionerà con cose come from x import y. Si può guardare qui per un'implementazione più complessa.

EDIT3

Si noti che molte funzionalità integrate in python non sono disponibili. fuori dalla scatola in RestrictedPython, non significa che non siano affatto disponibili. Potrebbe essere necessario implementare qualche funzione per renderla disponibile.

Anche alcune cose ovvie come sum o += non sono ovvie in un ambiente ristretto.

Ad esempio, l'operatore for utilizza _getiter_ che deve essere implementata e fornita dall'utente stesso (in globals). Poiché si vogliono evitare i loop infiniti, si potrebbero porre dei limiti al numero di iterazioni consentite. Ecco un esempio di implementazione che limita a 100 il numero di iterazioni:

MAX_ITER_LEN = 100

class MaxCountIter:
    def __init__(self, dataset, max_count):
        self.i = iter(dataset)
        self.left = max_count

    def __iter__(self):
        return self

    def __next__(self):
        if self.left > 0:
            self.left -= 1
            return next(self.i)
        else:
            raise StopIteration()

def _getiter(ob):
    return MaxCountIter(ob, MAX_ITER_LEN)

....

restricted_globals = {
    "_getiter_": _getiter,

....

for_ex = """
def sum(x):
    y = 0
    for i in range(x):
        y = y + i
    return y
"""

print(execute_user_code(for_ex, "sum", 6))

Se non si vuole limitare il numero di cicli, è sufficiente utilizzare la funzione identity come _getiter_:

restricted_globals = {
    "_getiter_": labmda x: x,

Si noti che la semplice limitazione del numero di cicli non garantisce la sicurezza. Innanzitutto, i cicli possono essere annidati. In secondo luogo, non è possibile limitare il numero di esecuzioni di una funzione while loop. Per renderlo sicuro, è necessario eseguire il codice non sicuro sotto un certo timeout.

Prendetevi un momento per leggere la documentazione.

Si noti che non tutto è documentato (anche se molte cose lo sono). Bisogna imparare a leggere il codice sorgente del progetto per le cose più avanzate. Il modo migliore per imparare è provare a eseguire del codice e vedere che tipo di funzione manca, poi vedere il codice sorgente del progetto per capire come implementarla.

EDIT4

C'è ancora un altro problema: il codice ristretto può avere loop infiniti. Per evitarlo, è necessario un qualche tipo di timeout nel codice.

Sfortunatamente, dato che si sta usando django, che è multi thread a meno che non si specifichi esplicitamente il contrario, il semplice trucco per i timeout usando i signeal non funzionerà qui, si deve usare il multiprocessing.

Il modo più semplice a mio parere - usa questa libreria. Basta aggiungere un decoratore a execute_user_code in modo che appaia come questo:

@timeout_decorator.timeout(5, use_signals=False)
def execute_user_code(user_code, user_func, *args, **kwargs):

E il gioco è fatto. Il codice non funzionerà mai per più di 5 secondi.
Prestare attenzione a use_signals=False, senza il quale si potrebbe avere un comportamento inaspettato in django.

Si noti anche che questo è relativamente pesante per le risorse (e non vedo un modo per superarlo). Cioè, non è un peso pazzesco, ma è un processo in più che viene generato. Dovreste tenerlo presente nella configurazione del vostro server web: l'API che permette di eseguire codice utente arbitrario è più vulnerabile ai ddos.

Sicuramente con docker è possibile fare sandboxing dell'esecuzione se si fa attenzione. Si possono limitare i cicli della CPU, la memoria massima, chiudere tutte le porte di rete, eseguire come utente con accesso in sola lettura al file system e tutto il resto).

Tuttavia, credo che sarebbe estremamente complesso farlo bene. Per me non si deve permettere a un client di eseguire codice arbitrario come questo.

Io vorrei verificare se una produzione/soluzione non è già stata fatta e usare quella. Stavo pensando che alcuni siti permettono di inviare del codice (python, java, o altro) che viene eseguito sul server.

valutazioni e commenti



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.