Injeção em Node.js
A injeção é uma das vulnerabilidades mais críticas em aplicações web. Em Node.js, ocorre quando dados não validados são interpretados como código ou comando pelo servidor. A forma mais comum é a injeção SQL, mas também temos injeção de NoSQL, OS command injection e template injection.
SQL Injection acontece quando concatenamos entrada do usuário diretamente em queries. No exemplo abaixo, um atacante pode manipular a lógica da consulta:
// ❌ VULNERÁVEL
const express = require('express');
const mysql = require('mysql');
const app = express();
app.get('/user/:id', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
// Entrada: 1 OR 1=1 -- causa retorno de todos os usuários
connection.query(query, (err, results) => {
res.json(results);
});
});
A defesa é usar prepared statements (queries parametrizadas), que separam código de dados:
// ✅ SEGURO
app.get('/user/:id', (req, res) => {
const query = 'SELECT * FROM users WHERE id = ?';
connection.query(query, [req.params.id], (err, results) => {
res.json(results);
});
});
NoSQL Injection é igualmente perigosa. Em MongoDB, objetos JavaScript são interpretados como filtros:
// ❌ VULNERÁVEL
app.post('/login', (req, res) => {
const user = await db.collection('users').findOne({
email: req.body.email,
password: req.body.password
});
// Entrada: {"$ne": ""} contorna autenticação
});
// ✅ SEGURO
app.post('/login', (req, res) => {
const user = await db.collection('users').findOne({
email: String(req.body.email),
password: String(req.body.password)
});
});
Sempre valide e sanitize inputs usando bibliotecas como joi, yup ou validator.js.
SSRF (Server-Side Request Forgery) e Path Traversal
SSRF ocorre quando sua aplicação faz requisições HTTP para URLs controladas pelo usuário, permitindo acesso a recursos internos. Um atacante pode acessar localhost, metadados AWS ou serviços internos:
// ❌ VULNERÁVEL
const axios = require('axios');
app.post('/fetch-url', (req, res) => {
axios.get(req.body.url).then(response => {
res.json(response.data);
});
// URL: http://localhost:6379 (Redis)
// URL: http://169.254.169.254/latest/meta-data (AWS)
});
// ✅ SEGURO
const url = require('url');
const axios = require('axios');
app.post('/fetch-url', (req, res) => {
try {
const parsedUrl = new URL(req.body.url);
// Whitelist de domínios permitidos
const whitelist = ['api.example.com', 'cdn.example.com'];
if (!whitelist.includes(parsedUrl.hostname)) {
return res.status(403).json({ error: 'Domain not allowed' });
}
// Bloqueie IPs privados
if (/^(localhost|127\.|10\.|172\.|192\.168\.)/.test(parsedUrl.hostname)) {
return res.status(403).json({ error: 'Private network access forbidden' });
}
axios.get(req.body.url).then(response => {
res.json(response.data);
});
} catch (err) {
res.status(400).json({ error: 'Invalid URL' });
}
});
Path Traversal permite acesso a arquivos fora do diretório permitido usando ../. Um atacante pode ler /etc/passwd ou arquivos privados:
// ❌ VULNERÁVEL
const fs = require('fs');
const path = require('path');
app.get('/file/:name', (req, res) => {
const filePath = `/uploads/${req.params.name}`;
// Entrada: ../../etc/passwd lê arquivo do sistema
fs.readFile(filePath, (err, data) => {
res.send(data);
});
});
// ✅ SEGURO
app.get('/file/:name', (req, res) => {
const baseDir = path.resolve('/uploads');
const filePath = path.resolve(path.join(baseDir, req.params.name));
// Valide se o arquivo resolvido está dentro do diretório base
if (!filePath.startsWith(baseDir)) {
return res.status(403).json({ error: 'Access denied' });
}
fs.readFile(filePath, (err, data) => {
if (err) return res.status(404).json({ error: 'File not found' });
res.send(data);
});
});
Use path.resolve() para normalizar caminhos e sempre valide se o resultado está dentro do diretório esperado.
Hardening e Boas Práticas
Hardening significa endurecer sua aplicação contra múltiplos vetores de ataque. Comece com validação robusta de inputs, uso de variáveis de ambiente para secrets, e headers de segurança HTTP:
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const validator = require('validator');
const app = express();
// Helmet adiciona headers de segurança (CSP, X-Frame-Options, etc)
app.use(helmet());
// Rate limiting previne força bruta
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100 // máximo 100 requisições
});
app.use('/api/', limiter);
// Validação rigorosa
app.post('/register', (req, res) => {
const { email, password } = req.body;
if (!validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
if (!validator.isLength(password, { min: 12 })) {
return res.status(400).json({ error: 'Password too weak' });
}
// Hash de senha com bcrypt
const bcrypt = require('bcrypt');
const hashedPassword = bcrypt.hashSync(password, 10);
// Salve no banco...
res.json({ success: true });
});
// CORS restritivo
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
Práticas adicionais essenciais: nunca exponha stack traces em produção, use logs estruturados, mantenha dependências atualizadas com npm audit, implemente autenticação forte (JWT com expiração curta), use HTTPS obrigatório, e considere WAF (Web Application Firewall) para ambientes críticos.
Conclusão
Os três pilares da segurança em Node.js são: validação rigorosa (sempre desconfie de inputs), separação de código e dados (prepared statements, whitelist), e hardening sistemático (headers, rate limiting, HTTPS). Vulnerabilidades como injeção, SSRF e path traversal continuam entre as mais exploradas porque desenvolvedores negligenciam essas fundações. Implemente essas práticas desde o design, não como patch posterior — segurança é construída, não adicionada.