Back

Tratamento Seguro de Entrada de Usuário em Node.js

Tratamento Seguro de Entrada de Usuário em Node.js

Toda API Node.js recebe dados nos quais não pode confiar plenamente. Corpos JSON, query strings, campos de formulário e uploads de arquivos chegam do mundo externo, e qualquer um deles pode carregar payloads maliciosos. Injeção SQL, injeção NoSQL, injeção de comando, poluição de protótipo — esses ataques compartilham um ponto de entrada comum: entrada de usuário não validada alcançando a lógica de negócio, um banco de dados ou um comando shell. Este artigo apresenta os padrões práticos que os impedem.

Pontos-Chave

  • Sempre valide dados de entrada contra um schema estrito antes que eles alcancem sua lógica de aplicação — defina o que é permitido e rejeite todo o resto.
  • Use consultas parametrizadas para SQL e imponha tipos de dados estritos para NoSQL para prevenir ataques de injeção.
  • Evite child_process.exec() para comandos de sistema; use execFile() ou spawn() com arrays de argumentos explícitos.
  • Nunca espalhe corpos de requisição brutos em models; selecione apenas os campos que você espera para prevenir atribuição em massa e poluição de protótipo.
  • Defina limites de tamanho de corpo e valide uploads de arquivo no nível do parser para proteger contra ataques de negação de serviço.

Valide Primeiro, Processe Depois

A regra mais eficaz no tratamento seguro de requisições em APIs Node.js é: nunca deixe dados brutos de requisição tocarem sua lógica de aplicação. Valide a forma e o tipo de cada valor de entrada antes de usá-lo.

Validação baseada em schema é a abordagem mais limpa. Bibliotecas como Zod e Joi permitem que você declare exatamente como uma requisição deve ser e rejeite qualquer coisa que não corresponda.

import { z } from "zod"

const RegisterSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  password: z.string().min(10),
})

app.post("/register", (req, res) => {
  const result = RegisterSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() })
  }
  // result.data agora é seguro para usar
})

Esta é uma abordagem de lista de permissões: você define o que é permitido, e todo o resto é rejeitado. É muito mais confiável do que tentar criar uma lista de negação de caracteres perigosos. O mesmo princípio é enfatizado no OWASP Input Validation Cheat Sheet.


Previna Ataques de Injeção

Injeção SQL e NoSQL

Nunca concatene entrada de usuário em consultas. Use declarações parametrizadas para SQL:

// ❌ Vulnerável
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`

// ✅ Seguro
const result = await db.query("SELECT * FROM users WHERE email = $1", [email])

Bancos de dados NoSQL têm seu próprio risco de injeção. Operadores MongoDB como $gt podem ser injetados através de corpos JSON se você passar objetos diretamente:

// ❌ Vulnerável — req.body.username poderia ser { $gt: "" }
User.find({ username: req.body.username })

// ✅ Seguro — imponha o tipo esperado primeiro
User.find({ username: String(req.body.username) })

Na prática, a validação de schema deve garantir que o campo seja uma string (por exemplo, com z.string()) antes de alcançar a camada de consulta.

Injeção de Comando

Se sua API chama comandos de sistema, evite child_process.exec() completamente — ele passa argumentos através de um shell e é trivialmente explorável. Use execFile() ou spawn() com arrays de argumentos explícitos:

import { execFile } from "child_process"

// ✅ Argumentos são passados diretamente, não interpretados por um shell
execFile("convert", [userSuppliedFilename, outputPath], (err, stdout) => {
  // manipular resultado
})

Ainda assim, valide e crie uma lista de permissões para os valores de argumento. Se a operação for de alto risco, execute-a dentro de um container isolado.


Evite Atribuição em Massa e Poluição de Protótipo

Atribuição em massa acontece quando você espalha um corpo de requisição diretamente em um model:

// ❌ Um usuário poderia enviar { role: "admin" }
const user = await User.create({ ...req.body })

// ✅ Selecione apenas os campos que você espera
const user = await User.create({
  username: result.data.username,
  email: result.data.email,
})

Poluição de protótipo é mais sutil. Mesclar objetos não confiáveis pode permitir que um atacante injete propriedades como __proto__ ou constructor.prototype que afetam cada objeto em seu processo. Evite utilitários de mesclagem profunda em dados brutos de requisição. Se você precisar mesclar, valide a entrada primeiro e crie objetos intermediários com Object.create(null) para que eles não herdem da cadeia de protótipo padrão. O OWASP Prototype Pollution Prevention Cheat Sheet fornece orientação adicional.


Limite o Tamanho e a Estrutura da Requisição

Payloads grandes ou profundamente aninhados podem causar problemas de negação de serviço. Defina limites de tamanho de corpo no nível do parser:

app.use(express.json({ limit: "50kb" }))

Para uploads de arquivo, use multer com limites explícitos de fileSize e files, e valide tipos MIME — nunca confie apenas na extensão do arquivo.


Conclusão

O tratamento seguro de entrada de usuário em Node.js se resume a um princípio: defina como a entrada válida deve ser, imponha isso na fronteira e nunca deixe nada mais passar. Validação de schema, consultas parametrizadas, passagem explícita de argumentos e seleção estrita de campos cobrem a maioria das superfícies de ataque do mundo real. Aplique esses padrões consistentemente na camada de requisição, antes que os dados alcancem qualquer lógica downstream, e a maioria das vulnerabilidades de classe de injeção simplesmente não terá onde se fixar.

Perguntas Frequentes

A validação de schema é uma forte primeira linha de defesa porque rejeita entrada malformada antes que ela alcance sua lógica. No entanto, ela deve ser combinada com consultas parametrizadas e execução segura de comandos. A validação garante que os dados tenham a forma e o tipo corretos, mas o código downstream também deve manipular esses dados com segurança para cobrir casos extremos que a validação sozinha pode perder.

Ambas as bibliotecas são maduras e amplamente usadas. Zod oferece suporte de primeira classe para TypeScript com forte inferência de tipos, tornando-o uma escolha natural para projetos TypeScript. Joi tem um histórico mais longo no ecossistema Express e oferece uma API rica e encadeável. Escolha com base nas preferências de linguagem do seu projeto e dependências existentes.

Evite executar funções de merge profundo em dados brutos de requisição. Valide a entrada com um schema estrito primeiro para que apenas campos conhecidos passem. Quando a mesclagem for necessária, crie objetos intermediários com Object.create(null) para produzir objetos sem uma cadeia de protótipo. Bibliotecas como lodash.merge tiveram vulnerabilidades de poluição de protótipo no passado, então sempre mantenha as dependências atualizadas.

Não há uma resposta universal, mas 100kb a 1MB cobre a maioria dos casos de uso de API JSON. Comece com um limite conservador como 100kb e aumente apenas se requisições legítimas exigirem mais. Para uploads de arquivo, configure limites separados através do seu parser multipart. O objetivo é rejeitar payloads anormalmente grandes cedo, antes que consumam memória ou tempo de processamento.

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