
Sandbox Híbrido — como evitar que uma IA exploda seu sistema
O problema
O TatuEngine tem um agente autopoiético com ToolUse — ele pode executar código, ler arquivos, escrever resultados. Se o agente for malicioso (ou alucinar), ele pode tentar:
../../etc/passwd— path traversal clássico- Roubo de credenciais (chaves SSH, tokens)
- Acesso ao kernel
- Escrita em diretórios de sistema — persistência
Eu precisava de um sistema que permitisse o agente trabalhar mas bloqueasse tudo que é perigoso, sem depender de container Docker ou VM.
A solução: Sandbox Híbrido, em Python puro, 3 zonas de isolamento, 51 testes passando.
As 3 zonas
O sandbox define 3 zonas de segurança:
| Zona | Leitura | Escrita | Tamanho máx |
|---|---|---|---|
| SAFE | diretórios de projeto | 500 MB | |
| RESTRICTED | home + drives de dados | 100 MB | |
| DENIED | .ssh/*, .git/*, *.key, *.pem, .env*, /etc, /sys |
— |
A inteligência tá no match hierárquico: um path no diretório de projetos cai em SAFE (leitura + escrita), mas uma chave SSH cai em DENIED mesmo estando dentro da home.
Path traversal detection
O sandbox detecta 3 formas de ataque:
# Path traversal clássico
if ".." in path.split(os.sep):
return SandboxResult.err(f"Path traversal detectado: {path}")
# URL-encoded traversal
if "%2e" in path.lower():
return SandboxResult.err(f"Path traversal codificado: {path}")
# Byte nulo
if "\x00" in path:
return SandboxResult.err("Caminho contém byte nulo")
O resolvedor usa Path.resolve() (que segue symlinks e resolve ..), depois verifica se o path absoluto começa com um prefixo permitido:
def _is_safe_prefix(self, resolved: Path) -> bool:
r_str = str(resolved)
for raw in self._safe_prefixes:
expanded = str(Path(raw).expanduser())
if r_str == expanded or r_str.startswith(expanded + "/"):
return True
return False
Isso impede o truque de criar um symlink apontando pra fora da zona permitida e acessar via ele — o resolve() segue o symlink e revela o path real.
Zonas DENIED de sistema
Paths críticos são bloqueados mesmo que não estejam na lista de patterns: /etc, /sys, /proc, /boot, /bin, /sbin, /lib. O /usr libera só /usr/share, o /var só /var/log.
Escrita: só SAFE
O check_write() só permite escrita em prefixos SAFE:
if allow_write:
if self.level == "restricted":
return SandboxResult.err(
f"Escrita em sandbox RESTRICTED não permitida: {resolved}")
if not in_safe and not in_restricted:
return SandboxResult.err(f"Escrita fora de SAFE: {resolved}")
Isso significa que o agente pode criar arquivos nos diretórios de projeto (SAFE), mas não pode modificar credenciais (DENIED) nem escrever nos drives de dados (RESTRICTED).
Testes
O sandbox tem 439 linhas de testes. Testa:
- Leitura em SAFE
- Escrita em SAFE
- Path traversal (
../etc/passwd) - Path traversal URL-encoded (
%2e%2e) - Byte nulo no path
- Leitura de DENIED (
.git/config, chaves SSH) - Escrita em RESTRICTED
- Symlink apontando pra fora
- Path muito longo (>4096)
- Busca em diretório
- Arquivos aninhados em subdiretórios
O que aprendi
Path.resolve()é a chave — sem ele, symlinks viram backdoor. Com ele, qualquer tentativa de desviar do prefixo é neutralizada.- 3 zonas > 2 zonas — SAFE/RESTRICTED/DENIED é mais útil que só permitido/bloqueado. O agente pode ler configs do sistema (RESTRICTED) sem poder modificá-las.
- Pattern matching hierárquico — o diretório de projetos é SAFE, mas
.sshé DENIED mesmo estando dentro da home. A ordem importa: SAFE > RESTRICTED > DENIED. - Zero containers — não precisa de Docker ou VM pra isolar uma IA. Python puro +
Path.resolve()+ pattern matching resolve 99% dos casos.