Skip to content

Wie kann ich den Code des Benutzers sicher in meiner Webanwendung akzeptieren und ausführen?

Dieses Problem kann auf verschiedene Weise gelöst werden, aber wir zeigen Ihnen die umfassendste Antwort für uns.

Lösung:

Das ist eine wichtige Frage. In Python ist Sandboxing nicht trivial.

Es ist einer der wenigen Fälle, in denen sich die Frage stellt, welche Version des Python-Interpreters Sie verwenden. Zum Beispiel generiert Jyton Java-Bytecode, und die JVM hat ihren eigenen Mechanismus, um Code sicher auszuführen.

Für CPython, den Standardinterpreter, gab es ursprünglich einige Versuche, einen eingeschränkten Ausführungsmodus zu schaffen, die aber vor langer Zeit aufgegeben wurden.

Gegenwärtig gibt es das inoffizielle Projekt RestrictedPython, das Ihnen geben könnte, was Sie brauchen. Es ist keine vollständige Sandbox, d.h. es gibt dir keinen eingeschränkten Zugriff auf das Dateisystem oder ähnliches, aber für deine Bedürfnisse könnte es gerade genug sein.

Im Grunde haben die Jungs dort nur die Python-Kompilierung auf eine eingeschränktere Weise umgeschrieben.

Was es erlaubt, ist, ein Stück Code zu kompilieren und dann auszuführen, alles in einem eingeschränkten Modus. Zum Beispiel:

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

Ausführen mit builtins = safe_builtins schaltet die gefährlichen Funktionen wie open file, import oder was auch immer. Es gibt auch andere Variationen von Builtins . und andere Optionen, nimm dir etwas Zeit, um die Dokumente zu lesen, sie sind ziemlich gut.

EDIT:

Hier ist ein Beispiel für Ihren Anwendungsfall

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

Jetzt haben Sie eine Funktion execute_user_code, die einen unsicheren Code als Zeichenkette, einen Namen einer Funktion aus diesem Code und Argumente erhält und den Rückgabewert der Funktion mit den angegebenen Argumenten zurückgibt.

Hier ist ein sehr dummes Beispiel für einen Benutzercode:

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

Aber hier ist, was passiert, wenn der Benutzercode versucht, etwas Unsicheres zu tun:

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

Mögliche Erweiterung:

Achten Sie darauf, dass der Benutzercode bei jedem Aufruf der Funktion kompiliert wird. Es ist jedoch möglich, dass Sie den Benutzercode einmal kompilieren und dann mit anderen Parametern ausführen möchten. Dazu müssen Sie lediglich die byte_code irgendwo zu speichern und dann exec mit einem anderen Satz von restricted_locals jedes Mal aufzurufen.

EDIT2:

Wenn man importieren möchte, kann man eine eigene Importfunktion schreiben, die es erlaubt, nur Module zu verwenden, die man als sicher ansieht. Beispiel:

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

Beachte, dass diese Beispiel-Importfunktion SEHR primitiv ist, sie wird nicht mit Dingen funktionieren wie from x import y. Für eine komplexere Implementierung können Sie hier nachsehen.

EDIT3

Beachten Sie, dass viele der in Python eingebauten Funktionen nicht verfügbar sind out of the box in RestrictedPython nicht verfügbar sind, bedeutet das nicht, dass sie überhaupt nicht verfügbar sind. Möglicherweise müssen Sie eine Funktion implementieren, um sie verfügbar zu machen.

Sogar einige offensichtliche Dinge wie sum oder . += Operator sind in der eingeschränkten Umgebung nicht offensichtlich.

Zum Beispiel kann der for Schleife verwendet _getiter_ Funktion, die Sie selbst implementieren und bereitstellen müssen (in globals). Da Sie Endlosschleifen vermeiden wollen, sollten Sie die Anzahl der zulässigen Iterationen begrenzen. Hier ist eine Beispielimplementierung, die die Anzahl der Iterationen auf 100 begrenzt:

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

Wenn Sie die Anzahl der Schleifen nicht begrenzen wollen, verwenden Sie einfach die Identitätsfunktion als _getiter_:

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

Beachten Sie, dass eine einfache Begrenzung der Schleifenzahl keine Sicherheit garantiert. Erstens können Schleifen verschachtelt werden. Zweitens können Sie die Anzahl der Ausführungen einer Schleife nicht begrenzen. while Schleife begrenzen. Um sie sicher zu machen, müssen Sie unsicheren Code nach einer gewissen Zeitspanne ausführen.

Bitte nehmen Sie sich einen Moment Zeit, um die Dokumente zu lesen.

Beachten Sie, dass nicht alles dokumentiert ist (obwohl viele Dinge es sind). Für fortgeschrittene Dinge müssen Sie lernen, den Quellcode des Projekts zu lesen. Der beste Weg, um zu lernen, ist zu versuchen, etwas Code auszuführen und zu sehen, welche Funktion fehlt, und dann den Quellcode des Projekts zu lesen, um zu verstehen, wie man sie implementiert.

EDIT4

Es gibt noch ein weiteres Problem - eingeschränkter Code kann Endlosschleifen enthalten. Um dies zu vermeiden, ist eine Art von Timeout im Code erforderlich.

Da du django verwendest, das multi threaded ist, sofern du nicht explizit etwas anderes angibst, funktioniert der einfache Trick für Timeouts mit Signals hier leider nicht, du musst Multiprocessing verwenden.

Der einfachste Weg meiner Meinung nach - verwenden Sie diese Bibliothek. Fügen Sie einfach einen Dekorator zu execute_user_code damit es so aussieht:

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

Und Sie sind fertig. Der Code wird nie länger als 5 Sekunden laufen.
Achten Sie auf use_signals=False, ohne dies kann es zu einem unerwarteten Verhalten in django kommen.

Beachten Sie auch, dass dies relativ ressourcenintensiv ist (und ich sehe nicht wirklich eine Möglichkeit, dies zu überwinden). Ich meine, nicht wirklich verrückt schwer, aber es ist ein zusätzlicher Prozess-Spawn. Sie sollten das in Ihrer Webserver-Konfiguration berücksichtigen - die API, die die Ausführung von beliebigem Benutzercode erlaubt, ist anfälliger für DDoS.

Sicherlich können Sie mit Docker die Ausführung in eine Sandbox verlagern, wenn Sie vorsichtig sind. Sie können CPU-Zyklen, maximalen Speicher beschränken, schließen Sie alle Netzwerk-Ports, laufen als Benutzer mit nur Lesezugriff auf das Dateisystem und alle).

Trotzdem wäre das extrem komplex, um es richtig zu machen, denke ich. Meiner Meinung nach darf man einem Client nicht erlauben, beliebigen Code wie diesen auszuführen.

Ich würde prüfen, ob es nicht schon eine Produktion/Lösung gibt und diese verwenden. Ich habe mir überlegt, dass es auf manchen Seiten die Möglichkeit gibt, einen Code (Python, Java, was auch immer) einzureichen, der dann auf dem Server ausgeführt wird.

Denken Sie daran, diesen Aufsatz zu zeigen, wenn Sie Erfolg hatten.



Nutzen Sie unsere Suchmaschine

Suche
Generic filters

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.