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

258 lines
8.9 KiB
Markdown

# 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.