Segurança de API em Produção: Guia Prático OWASP para Desenvolvedores
Proteja sua API contra as ameaças mais comuns: injeção SQL, autenticação fraca, exposição de dados, rate limiting, CORS mal configurado e ataques de enumeração.
Segurança não é uma feature — é um requisito
APIs expostas na internet são atacadas constantemente. Scanners automatizados testam para SQL injection, endpoints não autenticados, credenciais padrão e rate limiting ausente 24 horas por dia. OWASP API Security Top 10 é a referência mais importante para desenvolvedores de backend.
Este guia cobre as vulnerabilidades mais comuns com exemplos práticos de como evitá-las.
1. Injection (SQL, NoSQL, Command Injection)
O ataque: entrada do usuário interpretada como código.
// ❌ CRÍTICO: SQL injection
const { id } = req.params
const query = `SELECT * FROM users WHERE id = '${id}'`
// Atacante envia id = "1' OR '1'='1" → extrai todos os usuários
// ✅ Correto: query parametrizada
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
// ✅ Com ORM (Prisma, TypeORM) — nunca interpolem SQL:
const user = await prisma.user.findUnique({ where: { id } })
// ❌ Command injection
const filename = req.params.file
exec(`cat /uploads/${filename}`) // injeção de shell
// ✅ Correto: validar e escapar entrada, ou usar API de alto nível
const safePath = path.join('/uploads', path.basename(filename))
const content = await fs.readFile(safePath)
2. Autenticação e Autorização Rotas
// ❌ Verificação de autorização apenas no frontend
// O backend DEVE verificar permissões independentemente
// ✅ Middleware de autorização por recurso
async function canAccessOrder(req: Request, res: Response, next: NextFunction) {
const order = await db.orders.findUnique({ where: { id: req.params.id } })
if (!order) return res.status(404).json({ error: 'Não encontrado' })
// Verificar se o usuário logado é dono do pedido
if (order.userId !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Acesso negado' })
}
req.order = order
next()
}
// Aplicar em todas as rotas sensíveis
router.get('/orders/:id', authenticate, canAccessOrder, getOrder)
router.put('/orders/:id', authenticate, canAccessOrder, updateOrder)
router.delete('/orders/:id', authenticate, canAccessOrder, deleteOrder)
3. Exposição de dados sensíveis
// ❌ Expor campos desnecessários
const user = await db.users.findUnique({ where: { id } })
return res.json(user) // inclui password, salt, internal_notes, etc.
// ✅ Selecionar apenas campos necessários
const user = await db.users.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
// password: false (implícito - não incluído)
}
})
return res.json(user)
// ✅ Sanitização com schema de resposta (Zod)
const UserResponseSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
role: z.enum(['admin', 'user']),
})
const rawUser = await db.users.findUnique(...)
const safeUser = UserResponseSchema.parse(rawUser) // garante que campos extras são removidos
return res.json(safeUser)
4. Rate Limiting e proteção contra força bruta
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
// Rate limit geral
const generalLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 300,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({ client: redis }), // distribuído entre instâncias
})
// Rate limit mais restrito para endpoints sensíveis
const authLimit = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // máx 10 tentativas de login por 15 min por IP
message: { error: 'Muitas tentativas. Tente novamente mais tarde.' },
})
const passwordResetLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 3,
})
app.use(generalLimit)
app.post('/auth/login', authLimit, loginHandler)
app.post('/auth/forgot-password', passwordResetLimit, forgotPasswordHandler)
5. Validação de entrada com Zod
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(2).max(100).trim(),
email: z.string().email().toLowerCase(),
password: z.string()
.min(8)
.regex(/[A-Z]/, 'Deve conter pelo menos uma letra maiúscula')
.regex(/[0-9]/, 'Deve conter pelo menos um número'),
role: z.enum(['user', 'admin']).default('user'),
})
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({
error: 'Dados inválidos',
details: result.error.flatten().fieldErrors,
})
}
// result.data é tipado e validado
const user = await userService.create(result.data)
return res.status(201).json(user)
})
6. CORS configurado corretamente
import cors from 'cors'
// ❌ CORS aberto — nunca faça isso em produção
app.use(cors())
// ✅ CORS restritivo com origin whitelist
const allowedOrigins = [
'https://app.minha-empresa.com',
'https://www.minha-empresa.com',
...(process.env.NODE_ENV !== 'production' ? ['http://localhost:3000'] : []),
]
app.use(cors({
origin: (origin, callback) => {
// Permitir requests sem origin (mobile apps, Postman em dev)
if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error(`CORS: origem não permitida: ${origin}`))
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
}))
7. Headers de segurança com Helmet
import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000, // 1 ano
includeSubDomains: true,
preload: true,
},
}))
// Headers adicionais:
// X-Content-Type-Options: nosniff (helmet inclui)
// X-Frame-Options: DENY (helmet inclui)
// X-XSS-Protection: 1; mode=block (helmet inclui)
8. Prevenção de user enumeration
// ❌ Mensagens diferentes revelam se o usuário existe
if (!user) return res.status(404).json({ error: 'Usuário não encontrado' })
if (!passwordMatch) return res.status(401).json({ error: 'Senha incorreta' })
// ✅ Mesma mensagem para ambos os casos + mesmo tempo de resposta
// (use bcrypt.compare mesmo quando usuário não existe para equalizar timing)
const dummyHash = '$2b$10$invalidhashfortimingnormalization.....'
const user = await db.users.findUnique({ where: { email } })
const hash = user?.passwordHash || dummyHash
const isValid = user && await bcrypt.compare(password, hash)
if (!isValid) {
return res.status(401).json({ error: 'Credenciais inválidas' })
}
Checklist de segurança antes de ir ao ar
- [ ] Todas as queries usando ORM ou queries parametrizadas
- [ ] Rate limiting em endpoints de autenticação
- [ ] Validação de input com schema em todas as rotas
- [ ] Senha nunca retornada em nenhuma resposta
- [ ] CORS com whitelist explícita de origins
- [ ] Helmet configurado com CSP
- [ ] HTTPS forçado em produção (HSTS)
- [ ] JWT com algoritmo explícito e expiração curta
- [ ] Logs sanitizados (sem senhas, tokens, dados pessoais)
- [ ] Autorização verificada no backend para cada recurso
