Caminhos de arquivos parecem simples até a aplicação precisar funcionar em Linux, Windows, containers e ambientes de teste. Barras diferentes, diretórios relativos, extensões, nomes inesperados e entrada fornecida pelo usuário podem causar erros ou vulnerabilidades. O módulo Path no Node.js oferece funções nativas para construir, analisar, normalizar e comparar caminhos de maneira portável.
O módulo não acessa o sistema de arquivos. Ele apenas trabalha com strings que representam caminhos. Essa separação permite montar e validar localizações antes de chamar APIs como fs.readFile(), fs.writeFile() ou fs.stat().
Neste guia, você aprenderá a usar join(), resolve(), normalize(), basename(), dirname(), extname(), parse(), format(), relative(), as variantes POSIX e Windows e estratégias contra path traversal.
Importando o módulo Path
const path = require('node:path');Em ES Modules:
import path from 'node:path';A documentação oficial do módulo Path apresenta todas as funções e diferenças de plataforma. Para operações de arquivo, consulte File System no Node.js. A página da OWASP sobre path traversal descreve riscos de segurança relacionados.
Por que não concatenar caminhos manualmente?
Este código depende do separador e pode gerar barras duplicadas:
const file = baseDirectory + '/' + userDirectory + '/config.json';Com path.join(), o runtime utiliza as regras da plataforma:
const file = path.join(
baseDirectory,
userDirectory,
'config.json'
);Isso melhora portabilidade, mas não valida se userDirectory tenta sair da pasta permitida. Construção correta e autorização de caminho são problemas diferentes.
path.join()
join() combina segmentos e normaliza separadores:
const result = path.join(
'storage',
'users',
'123',
'avatar.png'
);
console.log(result);Segmentos vazios são ignorados. Se todos forem vazios, o resultado representa o diretório atual.
Normalização durante o join
const result = path.join(
'/srv/app',
'uploads',
'..',
'public',
'image.png'
);O segmento .. remove o diretório anterior durante a normalização. Isso é conveniente para caminhos controlados, mas perigoso quando partes vêm do usuário.
path.resolve()
resolve() produz um caminho absoluto, processando os segmentos da direita para a esquerda até encontrar uma raiz:
const absolute = path.resolve(
'storage',
'reports',
'daily.csv'
);
console.log(absolute);Quando nenhum segmento absoluto é encontrado, o diretório retornado por process.cwd() é usado como base.
Diferença entre join e resolve
path.join('/srv/app', '/uploads/file.txt');
path.resolve('/srv/app', '/uploads/file.txt');join() combina e normaliza. resolve() considera o último segmento absoluto como nova raiz. No exemplo, o segundo resultado ignora /srv/app. Essa diferença pode causar falhas de segurança se um valor externo começar com uma raiz.
path.normalize()
const normalized = path.normalize(
'/srv//app/uploads/../public/file.txt'
);A função remove separadores redundantes e resolve . e ... Ela não confirma que o arquivo existe e não impede que o resultado saia de um diretório permitido.
basename()
basename() retorna o último componente:
const name = path.basename('/srv/files/report.pdf');
console.log(name); // report.pdfTambém é possível remover uma extensão específica:
const name = path.basename(
'/srv/files/report.pdf',
'.pdf'
);
console.log(name); // reportA comparação da extensão respeita capitalização da string. Em sistemas de arquivos case-insensitive, isso pode ser diferente do comportamento real do disco.
dirname()
const directory = path.dirname(
'/srv/files/reports/daily.csv'
);
console.log(directory);A função retorna apenas a parte do diretório. Ela não cria a pasta. Para isso, use fs.mkdir() com a opção recursive.
extname()
console.log(path.extname('photo.png')); // .png
console.log(path.extname('archive.tar.gz')); // .gz
console.log(path.extname('.env')); // vazioextname() retorna a última extensão. Um arquivo oculto iniciado por ponto pode não ser tratado como extensão. Valide o formato de acordo com sua regra de negócio.
parse()
parse() decompõe um caminho:
const parsed = path.parse(
'/srv/files/archive.tar.gz'
);
console.log(parsed);O objeto contém root, dir, base, ext e name. Essa estrutura facilita alterar uma parte sem manipular a string inteira.
format()
const file = path.format({
dir: '/srv/reports',
name: 'daily',
ext: '.csv'
});format() monta um caminho a partir de um objeto. Quando base está presente, ele pode ter prioridade sobre name e ext, conforme a versão e as regras documentadas.
relative()
relative() calcula como chegar de um caminho a outro:
const relative = path.relative(
'/srv/app/src',
'/srv/app/tests/unit'
);
console.log(relative);O resultado pode conter ... Não o utilize diretamente como autorização para acesso.
isAbsolute()
path.isAbsolute('/srv/app');
path.isAbsolute('uploads/file.png');Detectar que um caminho é absoluto não significa que ele é seguro. Um caminho absoluto fornecido pelo cliente pode apontar para qualquer região acessível ao processo.
Separadores de diretório
console.log(path.sep);No POSIX, o separador normalmente é /. No Windows, é \. Para dividir listas de caminhos, como a variável PATH, use path.delimiter:
const entries = process.env.PATH
?.split(path.delimiter)
.filter(Boolean);path.posix e path.win32
Você pode processar caminhos de outra plataforma independentemente do sistema atual:
const linuxPath = path.posix.join(
'/srv',
'app',
'file.txt'
);
const windowsPath = path.win32.join(
'C:\\',
'app',
'file.txt'
);Isso é útil ao gerar arquivos de configuração, manifestos ou pacotes destinados a outra plataforma.
Caminhos em ES Modules
ES Modules não fornecem __dirname diretamente. Use URL:
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);Veja ES Modules no Node.js e URL API no Node.js para conversões corretas.
URLs não são caminhos
Não tente converter uma URL removendo manualmente file://. Caracteres escapados, espaços, drives e nomes UNC exigem regras próprias:
const filename = fileURLToPath(
new URL('./data.json', import.meta.url)
);Path traversal
Considere um endpoint de download:
const file = path.join(
uploadsDirectory,
req.params.filename
);Se o nome for ../../config.env, a normalização pode sair da pasta de uploads. Apenas usar join() não protege o acesso.
Validando contenção no diretório
function resolveInside(base, requested) {
const baseAbsolute = path.resolve(base);
const target = path.resolve(baseAbsolute, requested);
const relative = path.relative(baseAbsolute, target);
if (
relative.startsWith('..') ||
path.isAbsolute(relative)
) {
throw new Error('Caminho fora do diretório permitido');
}
return target;
}Essa validação impede saídas lexicais simples. Ainda é necessário considerar links simbólicos e alterações concorrentes no sistema de arquivos.
Links simbólicos
Um caminho aparentemente dentro da pasta pode apontar para fora através de symlink. Em operações sensíveis, obtenha o caminho real com fs.realpath() e compare novamente com a raiz real.
const realBase = await fs.realpath(baseDirectory);
const realTarget = await fs.realpath(target);
const relative = path.relative(realBase, realTarget);Para criação de novos arquivos, o destino pode ainda não existir. Avalie o caminho real do diretório pai, permissões e uso de flags que reduzam riscos de symlink.
Nomes de upload
Não preserve diretamente o nome enviado pelo usuário:
const safeName = `${crypto.randomUUID()}.bin`;
const destination = path.join(uploadDirectory, safeName);Armazene o nome original apenas como metadado sanitizado. Isso evita colisões, caracteres problemáticos e caminhos incorporados.
Extensão não confirma conteúdo
Um arquivo chamado photo.jpg pode conter outro formato. Verifique assinatura binária ou use uma biblioteca adequada. Também limite tamanho e armazene uploads fora da raiz pública quando possível.
Caminhos e arquivos estáticos
Ao servir conteúdo estático, defina uma raiz fixa e deixe o framework realizar validações. Não construa respostas a partir de um caminho arbitrário recebido pela URL.
Testes multiplataforma
test('cria caminho Windows', () => {
assert.equal(
path.win32.join('C:\\app', 'data', 'file.json'),
'C:\\app\\data\\file.json'
);
});Use path.posix e path.win32 em testes para verificar regras sem depender do sistema do CI.
Erros comuns
- Concatenar com barra: o código deixa de ser portável.
- Achar que normalize protege: path traversal continua possível.
- Confiar apenas em extensão: conteúdo malicioso pode usar nome permitido.
- Ignorar symlinks: o caminho real pode sair da raiz.
- Confundir URL e path: caracteres escapados são interpretados incorretamente.
- Usar cwd para arquivos do módulo: o serviço pode iniciar em outro diretório.
- Preservar nome de upload: colisões e caracteres perigosos aparecem.
Boas práticas para produção
- Use join e resolve em vez de concatenação.
- Defina raízes absolutas controladas.
- Valide que destinos permanecem dentro da raiz.
- Considere caminhos reais e symlinks.
- Gere nomes aleatórios para uploads.
- Converta URLs com fileURLToPath.
- Teste regras POSIX e Windows.
- Valide conteúdo, tamanho e permissões.
- Evite expor caminhos internos em erros.
- Separe construção de caminho da autorização.
Conclusão
O módulo Path no Node.js resolve diferenças entre plataformas e oferece ferramentas claras para montar e analisar caminhos. Funções como join, resolve, parse e relative tornam o código mais legível e testável.
Segurança exige etapas adicionais. Normalização não impede path traversal, extensões não validam conteúdo e symlinks podem alterar o destino real. Com raízes controladas, validação de contenção e nomes gerados pela aplicação, caminhos podem ser tratados de forma portável e segura.




