Sandboxing et isolation
Pourquoi isoler les agents IA ?
Un agent IA qui exécute du code ou appelle des outils est fondamentalement différent d’un chatbot simple. S’il est compromis par une injection, il peut exécuter des commandes système, lire des fichiers sensibles ou communiquer avec des services externes. Le sandboxing consiste à enfermer l’agent dans un environnement restreint pour limiter les dégâts en cas de compromission.
Niveaux d’isolation
Niveau 1 : Isolation des permissions (moindre privilège)
import os
from pathlib import Path
class SandboxPermissions:
"""Contrôle les permissions d'accès aux ressources."""
def __init__(self, dossier_autorise: str, commandes_autorisees: list[str]):
self.dossier_autorise = Path(dossier_autorise).resolve()
self.commandes_autorisees = set(commandes_autorisees)
def verifier_chemin(self, chemin: str) -> bool:
"""Vérifie qu'un chemin est dans le dossier autorisé."""
chemin_resolu = Path(chemin).resolve()
try:
chemin_resolu.relative_to(self.dossier_autorise)
return True
except ValueError:
return False
def verifier_commande(self, commande: str) -> bool:
"""Vérifie qu'une commande est dans la liste blanche."""
cmd_base = commande.split()[0] if commande else ""
return cmd_base in self.commandes_autorisees
def lire_fichier(self, chemin: str) -> str:
"""Lecture sécurisée avec vérification de chemin."""
if not self.verifier_chemin(chemin):
raise PermissionError(
f"Accès refusé : {chemin} est hors du dossier autorisé"
)
return Path(chemin).read_text()
# Utilisation
sandbox = SandboxPermissions(
dossier_autorise="/data/app/uploads",
commandes_autorisees=["ls", "cat", "wc"],
)
# OK : fichier dans le dossier autorisé
try:
contenu = sandbox.lire_fichier("/data/app/uploads/rapport.txt")
except PermissionError as e:
print(f"Bloqué : {e}")
# Bloqué : tentative de traversée de chemin
try:
contenu = sandbox.lire_fichier("/data/app/uploads/../../etc/passwd")
except PermissionError as e:
print(f"Bloqué : {e}")
Niveau 2 : Isolation réseau
import subprocess
class SandboxReseau:
"""Contrôle les communications réseau de l'agent."""
def __init__(self, domaines_autorises: list[str]):
self.domaines_autorises = set(domaines_autorises)
def requete_autorisee(self, url: str) -> bool:
"""Vérifie qu'une URL est dans la liste blanche."""
from urllib.parse import urlparse
domaine = urlparse(url).hostname
return domaine in self.domaines_autorises
def fetch_securise(self, url: str) -> str:
"""Requête HTTP avec vérification de domaine."""
if not self.requete_autorisee(url):
raise PermissionError(f"Domaine non autorisé : {url}")
import urllib.request
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.read().decode()
sandbox_net = SandboxReseau(
domaines_autorises=["api.openai.com", "api.anthropic.com"]
)
# OK
try:
sandbox_net.fetch_securise("https://api.openai.com/v1/models")
except PermissionError as e:
print(e)
# Bloqué : exfiltration vers un domaine externe
try:
sandbox_net.fetch_securise("https://attaquant.com/collect")
except PermissionError as e:
print(f"Bloqué : {e}")
Niveau 3 : Exécution de code en sandbox
import subprocess
import tempfile
import os
class SandboxExecution:
"""Exécute du code généré par le modèle dans un environnement isolé."""
def __init__(self, timeout: int = 10, max_memoire_mb: int = 256):
self.timeout = timeout
self.max_memoire = max_memoire_mb
def executer_python(self, code: str) -> dict:
"""Exécute du code Python dans un processus isolé."""
# Vérifications de sécurité avant exécution
imports_dangereux = ["os", "subprocess", "shutil", "socket", "requests"]
for imp in imports_dangereux:
if f"import {imp}" in code or f"from {imp}" in code:
return {
"succes": False,
"erreur": f"Import interdit : {imp}",
"sortie": "",
}
# Créer un fichier temporaire
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as f:
# Ajouter des restrictions au début du script
wrapper = f"""
import resource
import sys
# Limiter la mémoire
resource.setrlimit(resource.RLIMIT_AS, ({self.max_memoire * 1024 * 1024}, {self.max_memoire * 1024 * 1024}))
# Limiter le temps CPU
resource.setrlimit(resource.RLIMIT_CPU, ({self.timeout}, {self.timeout}))
# Code de l'utilisateur
{code}
"""
f.write(wrapper)
fichier_temp = f.name
try:
result = subprocess.run(
["python3", fichier_temp],
capture_output=True,
text=True,
timeout=self.timeout,
env={"PATH": "/usr/bin", "HOME": "/tmp"},
)
return {
"succes": result.returncode == 0,
"sortie": result.stdout[:5000],
"erreur": result.stderr[:2000] if result.returncode != 0 else "",
}
except subprocess.TimeoutExpired:
return {"succes": False, "erreur": "Timeout dépassé", "sortie": ""}
finally:
os.unlink(fichier_temp)
# Utilisation
sandbox = SandboxExecution(timeout=5, max_memoire_mb=128)
# OK : calcul simple
resultat = sandbox.executer_python("print(sum(range(100)))")
print(resultat) # {"succes": True, "sortie": "4950\n", "erreur": ""}
# Bloqué : tentative d'accès système
resultat = sandbox.executer_python("import os; os.listdir('/')")
print(resultat) # {"succes": False, "erreur": "Import interdit : os"}
Isolation avec Docker
Pour les cas de production, Docker fournit une isolation plus robuste :
class SandboxDocker:
"""Exécute du code dans un conteneur Docker éphémère."""
def __init__(self, image: str = "python:3.12-slim"):
self.image = image
def executer(self, code: str, timeout: int = 10) -> dict:
"""Exécute du code dans un conteneur isolé."""
cmd = [
"docker", "run", "--rm",
"--network=none", # Pas de réseau
"--memory=256m", # Limite mémoire
"--cpus=0.5", # Limite CPU
"--read-only", # Système de fichiers en lecture seule
"--tmpfs=/tmp:size=50m", # Espace temporaire limité
"--security-opt=no-new-privileges",
self.image,
"python3", "-c", code,
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout
)
return {
"succes": result.returncode == 0,
"sortie": result.stdout[:5000],
"erreur": result.stderr[:2000],
}
except subprocess.TimeoutExpired:
return {"succes": False, "erreur": "Timeout", "sortie": ""}
Matrice de décision : quel niveau d’isolation ?
| Scénario | Niveau recommandé |
|---|---|
| Chatbot Q&A simple | Permissions (niveau 1) |
| Agent avec accès API | Permissions + Réseau (niveaux 1-2) |
| Agent qui exécute du code | Docker complet (niveau 3) |
| Agent multi-utilisateurs | Docker + isolation par session |
Points clés à retenir
- Le sandboxing est indispensable dès qu’un agent a accès à des outils ou exécute du code
- Trois niveaux d’isolation : permissions, réseau, conteneur — combinez selon le risque
- Le principe de moindre privilège s’applique à chaque ressource : fichiers, réseau, commandes
- Docker avec
--network=noneet--read-onlyfournit l’isolation la plus robuste - L’isolation doit être testée régulièrement — un bypass de sandbox est un incident critique