In this article
Tratamento seguro de caminhos: por que operações seguras no sistema de arquivos são mais difíceis do que parecem
As vulnerabilidades de path traversal continuam aparecendo. As funções de E/S de arquivos da biblioteca padrão oferecidas pela maioria das linguagens foram projetadas para facilitar o uso, não para ambientes hostis. Quando seu código é executado em um sandbox, em um sistema compartilhado ou junto com entradas não confiáveis, essa lacuna se torna uma violação de limite de segurança.
A equipe de pesquisa de segurança da Snyk encontrou esse padrão repetidamente em diferentes ecossistemas:
Vulnerabilidades de alta gravidade no Incus que combinaram injeção de quebra de linha com ataques de symlink para permitir gravação arbitrária de arquivos e escalonamento de privilégios até root no host
Uma cadeia de escalonamento de privilégios no NixOS que explorou condições de corrida no armazenamento do Nix para manter acesso de gravação a caminhos que deveriam ser imutáveis
Escalonamento de privilégios no Ubuntu 24.04, do usuário padrão a root, combinando manipulação do sistema de arquivos com bugs em componentes privilegiados
Essas técnicas de ataque não são exóticas; são o resultado de código que verifica um caminho em uma etapa e o utiliza em outra, deixando um intervalo no qual um invasor pode substituir um destino seguro por um malicioso.
As três classes de vulnerabilidade
Path traversal
A clássica. Uma entrada fornecida pelo usuário, como ../../etc/passwd, escapa do diretório pretendido. A maioria dos desenvolvedores sabe que precisa ficar atenta a isso, mas o problema continua aparecendo com frequência porque validar caminhos é mais difícil do que parece. Verificações simples de strings não levam em conta codificação de URL, bytes nulos, normalização Unicode nem separadores de caminho específicos de cada plataforma.
# Looks safe, isn't safe
def read_user_file(filename):
path = os.path.join("/app/uploads", filename)
if not path.startswith("/app/uploads"):
raise ValueError("Access denied")
return open(path).read()
# filename = "../../etc/passwd"
# path = "/app/uploads/../../etc/passwd"
# os.path.join resolves this, but startswith still passes on some inputsAtaques com symlinks
Um invasor cria um link simbólico que aponta de um local ao qual seu código espera ter acesso para outro que deveria estar fora do alcance. Se seu código seguir symlinks (o comportamento padrão na maioria dos sistemas), ele lerá ou gravará no destino escolhido pelo invasor.
Isso é especialmente perigoso em ambientes compartilhados, diretórios temporários e limites entre contêineres e sandboxes, onde um invasor pode ter acesso limitado de gravação ao sistema de arquivos.
Condições de corrida TOCTOU
Time-of-Check to Time-of-Use (do momento da verificação ao momento do uso). Seu código valida um caminho e, em seguida, opera nele. Entre essas duas operações, um invasor troca o destino. A verificação é aprovada, mas a operação atinge outro arquivo.
# Classic TOCTOU vulnerability
if os.path.isfile(path) and not os.path.islink(path):
# Attacker swaps path for a symlink right here
with open(path, 'w') as f:
f.write(data) # Now writing to attacker's targetO problema fundamental é que operações baseadas em caminhos estão sujeitas a condições de corrida em sistemas com vários usuários ou processos. Entre quaisquer duas chamadas de sistema que façam referência ao mesmo caminho, o estado do sistema de arquivos pode mudar.
A solução: operações baseadas em descritores de arquivo
A abordagem correta combina openat(), O_NOFOLLOW e a família de chamadas de sistema f* (fstat, fchmod, fchown). A ideia principal é: depois que você tem um descritor de arquivo aberto, ele se refere a um objeto específico do sistema de arquivos, independentemente do que aconteça com o caminho.
O padrão funciona assim:
Comece com um diretório conhecido e seguro (como
/ou a raiz de um sandbox)Abra cada componente do caminho individualmente usando
openat()com o descritor de arquivo do diretório anteriorUse
O_NOFOLLOWpara rejeitar symlinks em cada etapaValide cada componente (sem
.., sem symlinks e com as permissões corretas)Use o descritor de arquivo final em todas as operações subsequentes
Isso elimina TOCTOU porque você nunca mais usa o caminho depois de abri-lo. Todas as operações passam pelo descritor de arquivo, que o kernel garante apontar para o mesmo inode, independentemente de alterações no caminho.
Como as linguagens lidam com isso
C/C++: controle total disponível
C e C++ oferecem acesso direto a openat() e O_NOFOLLOW, o que torna simples implementar a abordagem corretamente (embora o código fique extenso). A solução do Google para o ChromeOS (libbrillo/brillo/files/safe_fd.cc) é uma boa implementação de referência.
int safe_open(int dirfd, const char *component) {
return openat(dirfd, component,
O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
}Go: suporte na biblioteca padrão (desde o Go 1.24)
O Go adicionou os.Root à biblioteca padrão, oferecendo uma maneira segura e sem condições de corrida de acessar arquivos em uma árvore de diretórios. É o padrão-ouro em suporte no nível da linguagem.
// Go 1.24+ — safe by default
root, err := os.OpenRoot("/sandbox")
if err != nil {
log.Fatal(err)
}
defer root.Close()
f, err := root.Open("user/data.txt")
// Guaranteed to stay within /sandbox, TOCTOU-proofVale a pena ler a publicação do blog do Go sobre o design de os.Root para entender a justificativa e os casos extremos considerados.
Rust: boas primitivas, montagem manual
Rust disponibiliza openat por meio do crate nix e oferece suporte a O_NOFOLLOW. O crate cap-std fornece acesso ao sistema de arquivos baseado em capacidades, que impede path traversal por construção.
use cap_std::fs::Dir;
let root = Dir::open_ambient_dir("/sandbox", cap_std::ambient_authority())?;
let file = root.open("user/data.txt")?;
// Constrained to /sandboxPython: suporte parcial
O Python 3.11+ tem os.open() com os parâmetros O_NOFOLLOW e dir_fd, mas criar o padrão completo de navegação segura exige trabalho manual. Não há na biblioteca padrão um equivalente ao os.Root do Go.
import os
def safe_open_under(root_path, relative_path):
root_fd = os.open(root_path, os.O_RDONLY | os.O_DIRECTORY)
try:
components = relative_path.split('/')
current_fd = root_fd
for component in components[:-1]:
if component in ('', '.', '..'):
raise ValueError(f"Invalid path component: {component}")
next_fd = os.open(
component,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current_fd
)
if current_fd != root_fd:
os.close(current_fd)
current_fd = next_fd
return os.open(
components[-1],
os.O_RDONLY | os.O_NOFOLLOW,
dir_fd=current_fd
)
finally:
os.close(root_fd)Node.js: impossível na prática?
É aqui que as coisas ficam difíceis. O Node.js não disponibiliza openat() nem O_NOFOLLOW na biblioteca padrão. O módulo fs opera exclusivamente com caminhos, não com descritores de arquivo. Não há como realizar navegação de diretórios protegida contra TOCTOU usando apenas Node.js, sem complementos nativos.
O melhor que você pode fazer é usar fs.realpath() e depois verificar o prefixo, mas isso está sujeito a condições de corrida. Um complemento nativo usando N-API poderia disponibilizar openat(), mas isso exige compilação e traz complexidade específica de cada plataforma.
Essa é uma lacuna significativa em um runtime amplamente usado em ferramentas de CLI, sistemas de build e ambientes conteinerizados, onde a segurança dos caminhos é importante.
Quando isso importa?
Nem todo aplicativo precisa de operações com arquivos protegidas contra TOCTOU. Se seu código só abre arquivos usando caminhos definidos diretamente no código ou é executado apenas em um ambiente de usuário único, sem entradas não confiáveis, as funções da biblioteca padrão são suficientes.
Você deve se preocupar com o tratamento seguro de caminhos quando:
Seu código é executado em um sandbox ou contêiner, e as operações no sistema de arquivos cruzam limites de confiança
Seu código processa caminhos de arquivos fornecidos pelo usuário (uploads, arquivos de configuração, diretórios de plugins)
Seu código é executado com privilégios elevados e acessa caminhos influenciados por usuários sem privilégios
Seu código opera em ambientes compartilhados (sistemas multi-inquilino, pipelines de CI/CD, sistemas de build)
Seu código lida com arquivos temporários em diretórios com permissão de gravação para todos, como
/tmp
Ações recomendadas
Audite sua base de código em busca de operações com arquivos baseadas em caminhos que recebem dados do usuário
Substitua verificações baseadas em caminhos (como
startswith()apósrealpath()) por navegação baseada em descritores de arquivo sempre que possívelNo Go 1.24+, use
os.Rootpara qualquer acesso a arquivos em sandboxEm Rust, considere o crate
cap-stdpara acessar o sistema de arquivos com base em capacidadesNo Node.js, reconheça a limitação e implemente uma estratégia de defesa em profundidade (validação de entradas, chroot, perfis do AppArmor/SELinux)
Use Snyk Code para detectar padrões de path traversal na sua base de código durante o desenvolvimento
Falhas ocultas no tratamento de arquivos, como path traversal, estão expondo silenciosamente seus aplicativos a comprometimentos mais graves? Baixe o whitepaper A crise de segurança de IA no seu ambiente Python para saber como equipes modernas estão prevenindo esses riscos tanto em código gerado por IA quanto em código escrito por desenvolvedores.
WHITE PAPER
A crise de segurança da IA no seu ambiente Python
Com o ritmo de desenvolvimento acelerando cada vez mais, você sabe o que o seu ambiente de IA pode acessar?