prometeu-runtime/docs/pull-requests/PR-004-runtime-vfs-path-traversal-hardening.md

8.9 KiB

PR-004: Runtime VFS Path Traversal Hardening

Briefing

Hoje o VirtualFS normaliza barras, mas nao canonicaliza nem rejeita segmentos ... Em seguida, o backend de host concatena o caminho recebido com a raiz montada. Isso abre espaco para escapar da raiz virtual com caminhos como /user/../../outside.txt.

Este PR endurece a fronteira entre o filesystem virtual e o filesystem do host. O objetivo e garantir que toda operacao de read/write/delete/list/exists permaneça estritamente dentro da raiz montada.

Problema

  • O VirtualFS aceita caminhos relativos e absolutos sem validacao estrutural suficiente.
  • O backend HostDirBackend faz root.join(path) sem bloquear traversal.
  • O problema afeta confidencialidade, integridade e isolamento do runtime.

Escopo

  • Endurecer a normalizacao de caminhos no VirtualFS.
  • Endurecer a resolucao no HostDirBackend.
  • Garantir comportamento consistente para read_file, write_file, delete, list_dir e exists.
  • Cobrir casos de traversal em testes unitarios.

Fora de Escopo

  • Suporte a links simbolicos com politicas avancadas.
  • Politicas de permissao por namespace (/system, /user, /apps, etc).
  • Refactor completo da API de filesystem.

Abordagem

  1. Introduzir uma regra unica de validacao de caminho virtual:
    • converter \ para /;
    • exigir caminho absoluto virtual;
    • colapsar . quando aparecer;
    • rejeitar qualquer segmento vazio ambiguo ou ..;
    • retornar erro explicito em vez de tentar "corrigir" traversal.
  2. Fazer o VirtualFS operar apenas sobre caminhos validados.
  3. Endurecer o HostDirBackend para nunca confiar apenas na normalizacao acima:
    • resolver o caminho relativo a partir da raiz;
    • rejeitar novamente qualquer tentativa de escapar;
    • manter defesa em profundidade mesmo se outro backend ou chamador evoluir errado.
  4. Garantir que operacoes booleanas como exists nao silenciem traversal como se fosse "arquivo inexistente" sem distinguir erro estrutural quando isso for relevante para a API.

Algoritmo

Normalizacao de caminho virtual

Entrada: path: &str

Saida: caminho virtual sanitizado ou erro.

Passos:

  1. Substituir \ por /.
  2. Se o caminho nao comecar com /, prefixar /.
  3. Separar por /.
  4. Ignorar segmentos vazios e ..
  5. Se algum segmento for .., falhar com FsError.
  6. Reconstruir o caminho como /<seg1>/<seg2>/....
  7. Preservar / como raiz quando nao houver segmentos.

Resolucao no backend do host

Entrada: caminho virtual sanitizado.

Saida: PathBuf dentro de root ou erro.

Passos:

  1. Remover o / inicial do caminho virtual.
  2. Concatenar cada segmento validado manualmente em um PathBuf iniciado em root.
  3. Nunca aceitar segmentos .., . ou componentes de prefixo/plataforma.
  4. Antes de retornar, garantir que o caminho construido continua sob root.

Plano de Execucao

Gate arquitetural

Antes de iniciar a implementacao, esta PR deve congelar a seguinte decisao:

  • exists permanece com retorno bool nesta PR.
  • Consequencia: caminho invalido ou tentativa de traversal em exists deve resultar em false, sem acesso ao host.
  • Justificativa: mudar exists para Result<bool, FsError> propaga alteracao de contrato para FsBackend, VirtualFS, runtime e syscall, o que expande o escopo desta PR.

Se essa decisao nao for aceita, a PR deve voltar para discussao arquitetural antes de qualquer alteracao de codigo.

Fase 1 - Endurecer contrato de erro

Arquivos alvo:

  • crates/console/prometeu-system/src/services/fs/fs_error.rs

Passos:

  1. Introduzir um erro explicito para caminho invalido, por exemplo FsError::InvalidPath(String) ou equivalente.
  2. Garantir mensagem clara para os casos:
    • caminho com ..;
    • componente de plataforma/prefixo inesperado;
    • caminho vazio estruturalmente invalido, se a implementacao optar por rejeita-lo.

Objetivo:

  • impedir que traversal seja reportado genericamente como IOError ou Other.

Fase 2 - Endurecer o VirtualFS

Arquivos alvo:

  • crates/console/prometeu-system/src/services/fs/virtual_fs.rs

Passos:

  1. Trocar normalize_path(&self, path: &str) -> String por uma funcao fallible:
    • normalize_path(path: &str) -> Result<String, FsError>.
  2. Implementar a normalizacao unica da camada virtual:
    • converter \ para /;
    • garantir raiz virtual absoluta;
    • colapsar . e segmentos vazios internos irrelevantes;
    • rejeitar ...
  3. Fazer list_dir, read_file, write_file e delete falharem antes de tocar no backend quando o caminho for invalido.
  4. Manter exists como fronteira booleana:
    • se a normalizacao falhar, retornar false;
    • nao chamar o backend nesse caso.

Objetivo:

  • estabelecer uma unica regra de caminho virtual para toda a API publica do VFS.

Fase 3 - Endurecer o HostDirBackend

Arquivos alvo:

  • crates/host/prometeu-host-desktop-winit/src/fs_backend.rs

Passos:

  1. Trocar resolve(&self, path: &str) -> PathBuf por uma resolucao fallible.
  2. Iterar manualmente pelos componentes do caminho e rejeitar:
    • ParentDir;
    • CurDir;
    • RootDir;
    • Prefix(_) em plataformas que exponham prefixos.
  3. Construir o PathBuf a partir de root apenas com segmentos normais.
  4. Validar ao final que o caminho produzido continua estritamente sob root.
  5. Fazer list_dir, read_file, write_file e delete retornarem erro estrutural em vez de acessar o host.
  6. Fazer exists retornar false para caminhos invalidos, sem side effect.

Objetivo:

  • manter defesa em profundidade mesmo que outra camada no futuro normalize errado.

Fase 4 - Cobertura de testes no prometeu-system

Arquivos alvo:

  • crates/console/prometeu-system/src/services/fs/virtual_fs.rs

Passos:

  1. Adicionar testes unitarios de normalizacao/rejeicao para:
    • ../x
    • /../x
    • /user/../../x
    • \\user\\..\\..\\x
  2. Adicionar um teste que prove que exists("../x") == false.
  3. Adicionar um mock backend com contadores ou flags para comprovar que caminhos invalidos:
    • nao chegam em read_file;
    • nao chegam em write_file;
    • nao chegam em delete;
    • nao chegam em list_dir;
    • nao chegam em exists.

Objetivo:

  • provar que a barreira virtual bloqueia traversal antes de qualquer backend.

Fase 5 - Cobertura de testes no host

Arquivos alvo:

  • crates/host/prometeu-host-desktop-winit/src/fs_backend.rs

Passos:

  1. Adicionar teste positivo de round-trip:
    • criar /user/test.txt;
    • ler;
    • verificar exists;
    • apagar.
  2. Adicionar teste de traversal em leitura e escrita usando diretorio temporario:
    • garantir erro para /user/../../outside.txt;
    • garantir que nenhum arquivo e criado fora de root.
  3. Adicionar teste para exists e list_dir com traversal:
    • exists retorna false;
    • list_dir retorna erro;
    • nenhum acesso fora da raiz e observado.

Objetivo:

  • provar que a defesa em profundidade do backend funciona mesmo isoladamente.

Fase 6 - Validacao final

Comandos:

  • cargo test -p prometeu-system
  • cargo test -p prometeu-host-desktop-winit

Checklist de saida:

  • nenhuma operacao acessa caminho fora da raiz montada;
  • caminhos validos continuam funcionais;
  • traversal falha de forma deterministica e explicita;
  • exists preserva contrato booleano sem vazar acesso ao host.

Sequencia recomendada

  1. Implementar FsError primeiro.
  2. Endurecer VirtualFS e fechar os testes unitarios da camada virtual.
  3. Endurecer HostDirBackend com defesa em profundidade.
  4. Adicionar os testes de host.
  5. Rodar os testes dos dois crates.

Riscos de execucao

  • O comportamento atual aceita caminhos permissivos como user/file.txt; apos esta PR eles continuarao aceitos apenas se normalizarem para uma forma absoluta segura.
  • A maior fonte de expansao de escopo e tentar mudar o contrato de exists; esta PR nao deve fazer isso.
  • Se surgir necessidade de suportar links simbolicos ou canonicalizacao real de host, isso caracteriza outra PR.

Criterios de Aceite

  • Qualquer tentativa de traversal com .. e rejeitada em read_file.
  • Qualquer tentativa de traversal com .. e rejeitada em write_file.
  • Qualquer tentativa de traversal com .. e rejeitada em delete.
  • exists e list_dir nao acessam caminhos fora da raiz montada.
  • Caminhos normais como /user/save.dat continuam funcionando.
  • O backend de host continua criando subdiretorios validos dentro da raiz.

Tests

  • Teste unitario no VirtualFS para rejeitar:
    • ../x
    • /../x
    • /user/../../x
    • \\user\\..\\..\\x
  • Teste unitario no backend do host validando que um caminho de traversal nao resulta em acesso fora da raiz temporaria.
  • Teste positivo para operacoes validas:
    • criar arquivo em /user/test.txt;
    • ler o mesmo arquivo;
    • confirmar exists;
    • apagar o arquivo.
  • Rodar:
    • cargo test -p prometeu-system
    • cargo test -p prometeu-host-desktop-winit

Risco

Baixo para a arquitetura e medio para compatibilidade, porque caminhos hoje aceitos de forma permissiva podem passar a falhar explicitamente. Esse endurecimento e desejado.