Sandbox Híbrido — como evitar que uma IA exploda seu sistema
TatuEngine·

Sandbox Híbrido — como evitar que uma IA exploda seu sistema

4 min de leitura← Voltar para timeline

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

  1. Path.resolve() é a chave — sem ele, symlinks viram backdoor. Com ele, qualquer tentativa de desviar do prefixo é neutralizada.
  2. 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.
  3. Pattern matching hierárquico — o diretório de projetos é SAFE, mas .ssh é DENIED mesmo estando dentro da home. A ordem importa: SAFE > RESTRICTED > DENIED.
  4. Zero containers — não precisa de Docker ou VM pra isolar uma IA. Python puro + Path.resolve() + pattern matching resolve 99% dos casos.