Back

Prevenindo Ataques de Travessia de Caminho em Node.js

Prevenindo Ataques de Travessia de Caminho em Node.js

Se sua aplicação Express serve arquivos baseados em entrada do usuário—uma rota de download, uma prévia de upload, um recurso dinâmico—você tem uma potencial vulnerabilidade de travessia de caminho. É um dos erros de segurança de arquivos mais comuns e prejudiciais em Node.js, e é surpreendentemente fácil de introduzir sem perceber.

Este artigo explica como funcionam os ataques de travessia de caminho, por que “correções” comuns ficam aquém do necessário, e como é realmente o tratamento seguro de arquivos em Node.js na prática.

Pontos-Chave

  • Ataques de travessia de caminho exploram validação ausente ou incompleta de caminhos de arquivo fornecidos pelo usuário, permitindo que atacantes acessem arquivos fora dos diretórios pretendidos.
  • path.normalize() e path.join() não são ferramentas de segurança—eles limpam caminhos, mas não previnem travessia de diretório.
  • A defesa correta é resolver caminhos com path.resolve() e verificar se o resultado permanece dentro de um diretório base permitido usando uma verificação startsWith que inclui path.sep.
  • A abordagem mais segura é evitar completamente caminhos fornecidos pelo usuário, mapeando a entrada para arquivos através de uma busca baseada em ID.

O Que É um Ataque de Travessia de Caminho?

Um ataque de travessia de caminho (também chamado de travessia de diretório) acontece quando um atacante manipula uma entrada de caminho de arquivo para acessar arquivos fora do diretório pretendido. O exemplo clássico:

GET /download?file=../../etc/passwd

Se seu servidor ingenuamente junta essa entrada com um diretório base e lê o arquivo, você acabou de entregar o arquivo de senhas do seu sistema.

O ataque não é sofisticado. Ele explora validação de entrada ausente ou incompleta—especificamente, a falha em verificar que um caminho resolvido permanece dentro de um diretório permitido.

Por Que path.normalize() e path.join() Não São Suficientes

Esta é a coisa mais importante para entender sobre prevenção de travessia de caminho em Node.js: esses utilitários não são ferramentas de segurança.

path.normalize() resolve sequências .. e limpa barras redundantes. path.join() concatena segmentos. Nenhum dos dois previne travessia—eles apenas produzem um caminho mais limpo que ainda pode apontar para fora do seu diretório pretendido.

Considere este padrão comum, mas inseguro:

// ❌ Inseguro: path.join não previne travessia
app.get('/download', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.query.file)
  res.sendFile(filePath)
})

Se req.query.file for ../../etc/passwd, path.join() resolve isso de forma limpa—e envia o arquivo errado.

Codificação de URL piora a situação. Um atacante pode enviar ..%2F..%2Fetc%2Fpasswd. O Express decodifica parâmetros de URL automaticamente, então você deve validar o valor decodificado que recebe. Se você estiver manipulando URLs brutas por conta própria, decodifique uma vez (com tratamento de erro apropriado) antes da validação.

O Padrão Correto: Resolver e Verificar Contenção

A abordagem confiável para tratamento seguro de arquivos em Node.js é:

  1. Resolver o caminho fornecido pelo usuário contra seu diretório base usando path.resolve().
  2. Verificar que o caminho resolvido permanece dentro daquele diretório.
const path = require('path')

const BASE_DIR = path.resolve(__dirname, 'uploads')

function safeResolve(userInput) {
  const resolved = path.resolve(BASE_DIR, userInput)

  // Garante que o caminho resolvido está dentro de BASE_DIR
  if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
    return null
  }

  return resolved
}

// ✅ Rota segura de download de arquivo no Express
app.get('/download', (req, res) => {
  const safePath = safeResolve(req.query.file)

  if (!safePath) {
    return res.status(403).send('Acesso negado')
  }

  res.sendFile(safePath)
})

Note a adição de path.sep. Sem ela, um diretório base de /uploads incorretamente permitiria que /uploads-other/secret.txt passasse pela verificação de prefixo.

No Windows, path.resolve() e path.sep lidam com barras invertidas corretamente, então este padrão funciona em todas as plataformas. Para validação mais rigorosa, você também pode comparar caminhos usando path.relative() e rejeitar qualquer resultado que escape do diretório base.

Esteja ciente de que links simbólicos ainda podem apontar para fora do seu diretório base. Se você estiver servindo arquivos sensíveis, resolva caminhos reais com fs.realpath() antes de enviá-los.

Melhor Ainda: Evite Completamente Caminhos Fornecidos pelo Usuário

A abordagem mais segura é não usar caminhos fornecidos pelo usuário de forma alguma. Em vez disso, mapeie a entrada do usuário para arquivos indiretamente:

// ✅ Busca de arquivo baseada em ID — sem construção de caminho a partir de entrada do usuário
const FILES = {
  'report-2024': 'reports/annual-2024.pdf',
  'invoice-001': 'invoices/inv-001.pdf',
}

app.get('/download/:id', (req, res) => {
  const filePath = FILES[req.params.id]

  if (!filePath) {
    return res.status(404).send('Não encontrado')
  }

  res.sendFile(path.resolve(__dirname, 'secure', filePath))
})

Isso elimina completamente o risco de travessia. Se seu caso de uso permitir—downloads de arquivos, exportações de documentos, recursos específicos do usuário—este é o padrão a buscar primeiro.

Uma Nota Sobre Servir Arquivos Estáticos no Express

express.static() é geralmente mais seguro para servir recursos públicos porque restringe o acesso a um diretório raiz definido. O risco aparece quando você escreve rotas personalizadas de servir arquivos que constroem caminhos a partir de parâmetros de requisição ou expõem diretórios não intencionais. Configuração e seleção de raiz ainda importam.

Conclusão

Travessia de caminho é uma vulnerabilidade direta com uma correção direta: nunca confie na entrada do usuário como um caminho de arquivo. Resolva-a contra uma base fixa, verifique a contenção com path.resolve() e uma verificação startsWith que inclui path.sep, e valide a entrada decodificada antes do uso. Quando possível, prefira buscas baseadas em ID que evitam a construção de caminho a partir de entrada do usuário completamente. path.normalize() e path.join() são utilitários úteis—apenas não são garantias de segurança.

Perguntas Frequentes

Ele ajuda ao servir arquivos de um diretório raiz definido e lidar com resolução de caminho internamente, mas não é uma garantia de segurança completa. Configuração incorreta ou exposição de diretórios sensíveis ainda podem criar risco. Rotas personalizadas de servir arquivos que usam entrada do usuário são onde a maioria das vulnerabilidades ocorre.

Não. Filtrar ou rejeitar strings que contêm sequências de ponto-ponto é frágil. Atacantes podem contornar tais verificações com codificação de URL, codificação dupla ou separadores de caminho específicos da plataforma. A defesa confiável é resolver o caminho completo com path.resolve() e então verificar se o resultado cai dentro do seu diretório base pretendido usando uma verificação startsWith ou path.relative().

Sim. Usar path.resolve() e path.sep faz a verificação de contenção funcionar em todas as plataformas. path.resolve() normaliza caminhos do Windows, e path.sep avalia para o separador correto. Para validação mais rigorosa, use path.relative() para garantir que o caminho resolvido não escape do diretório base.

Sim. Depois de confirmar que o caminho resolvido está dentro do seu diretório base, verifique se o arquivo existe usando fs.existsSync() ou fs.access() antes de chamar res.sendFile(). Isso previne vazamento de informações sobre a estrutura do seu diretório através de mensagens de erro e lhe dá controle sobre a resposta quando um arquivo está ausente.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay