Introdução: Por que APIs Requerem Segurança Específica
As APIs (Application Programming Interfaces) tornaram-se a espinha dorsal da arquitetura moderna de software. Diferentemente de aplicações web tradicionais, as APIs expõem funcionalidades de forma programática, frequentemente sem interface visual, o que cria um vetor de ataque completamente diferente. O OWASP API Security Top 10 foi criado justamente porque as vulnerabilidades específicas de APIs não são simples adaptações das vulnerabilidades web conhecidas — elas exigem compreensão profunda de como autenticação, autorização e validação funcionam em contextos de integração máquina-a-máquina.
Trabalhar com segurança de APIs não é opcional em 2024. Dados mostram que mais de 80% das brechas de segurança envolvem APIs em algum ponto da cadeia. Neste artigo, vamos dissecar as vulnerabilidades principais e entender como uma aplicação real é impactada, com exemplos práticos que você pode reproduzir e aprender.
Broken Object Level Authorization (BOLA) — A1
O Conceito Fundamental
BOLA ocorre quando a API não valida corretamente se o usuário autenticado tem permissão para acessar um recurso específico. Diferente da autenticação (sou quem digo ser), a autorização (posso fazer o que estou tentando fazer) é frequentemente negligenciada. Um ataque BOLA típico é quando um usuário modifica um parâmetro de ID na requisição para acessar dados de outro usuário.
Imagine uma API de banco que retorna informações de conta. Se você faz uma requisição GET /api/accounts/123/balance e consegue acessar dados da conta 124 apenas mudando o ID, você sofreu um ataque BOLA. O problema é que o desenvolvedor confiou que apenas usuários autenticados poderiam fazer requisições, mas não validou se aquele usuário específico pode acessar aquela conta específica.
Exemplo Vulnerável em Node.js/Express
// ❌ CÓDIGO VULNERÁVEL
const express = require('express');
const app = express();
app.get('/api/accounts/:accountId/balance', (req, res) => {
const { accountId } = req.params;
const user = req.user; // Usuário autenticado
// Apenas verifica se está autenticado, NÃO SE TEM ACESSO À CONTA
if (!user) {
return res.status(401).json({ error: 'Não autenticado' });
}
// Busca a conta sem validar propriedade
const account = database.getAccount(accountId);
res.json({ balance: account.balance });
});
Um atacante autenticado pode fazer requisições para qualquer accountId e obter dados de contas que não são suas.
Implementação Segura
// ✅ CÓDIGO SEGURO
app.get('/api/accounts/:accountId/balance', (req, res) => {
const { accountId } = req.params;
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Não autenticado' });
}
// Busca a conta
const account = database.getAccount(accountId);
// Valida se a conta pertence ao usuário
if (!account || account.userId !== user.id) {
return res.status(403).json({ error: 'Acesso negado' });
}
res.json({ balance: account.balance });
});
A diferença crucial: verificamos explicitamente se account.userId === user.id. Sem essa validação, qualquer usuário autenticado acessa qualquer conta.
Broken Authentication — A2
Por Que APIs Quebram Autenticação Diferente
Autenticação em APIs enfrenta desafios únicos. Não há sessão HTTP tradicional (cookies), então o token é enviado a cada requisição. Implementações fracas incluem: tokens sem expiração, falta de validação de assinatura, ausência de rate limiting, e implementações customizadas que reinventam a roda (perigosamente).
As falhas mais comuns são: permitir múltiplas tentativas de senha sem limite, aceitar credenciais em query strings (que aparecem em logs), não validar tokens JWT apropriadamente, e permitir reutilização de tokens mesmo após logout.
Exemplo Vulnerável: JWT Sem Validação Apropriada
// ❌ CÓDIGO VULNERÁVEL
const jwt = require('jsonwebtoken');
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (validateCredentials(username, password)) {
// Token sem expiração
const token = jwt.sign(
{ userId: user.id, username: user.username },
'secret-key' // Segredo hardcoded e fraco
);
res.json({ token });
}
});
app.get('/api/protected', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
// Decodifica sem validar assinatura em casos específicos
const decoded = jwt.decode(token); // ⚠️ NÃO valida!
res.json({ data: 'Segredo do usuário ' + decoded.userId });
} catch (e) {
res.status(401).json({ error: 'Token inválido' });
}
});
Problemas: token sem expiração, segredo fraco, jwt.decode() não valida assinatura (um atacante pode craftar um token falso). Um atacante pode falsificar um token completamente.
Implementação Segura
// ✅ CÓDIGO SEGURO
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Segredo forte, em variável de ambiente
const SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const maxAttempts = 5;
// Rate limiting simples
const attempts = cache.get(`login_attempts_${username}`) || 0;
if (attempts >= maxAttempts) {
return res.status(429).json({ error: 'Muitas tentativas. Tente novamente após 15 minutos.' });
}
if (!validateCredentials(username, password)) {
cache.set(`login_attempts_${username}`, attempts + 1, 900); // 15 minutos
return res.status(401).json({ error: 'Credenciais inválidas' });
}
// Token COM expiração e algoritmo seguro
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET,
{
expiresIn: '1h', // Expiração obrigatória
algorithm: 'HS256'
}
);
res.json({ token, expiresIn: 3600 });
});
app.get('/api/protected', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token não fornecido' });
}
const token = authHeader.split(' ')[1];
try {
// Valida assinatura E expiração
const decoded = jwt.verify(token, SECRET);
res.json({ data: 'Segredo do usuário ' + decoded.userId });
} catch (e) {
res.status(401).json({ error: 'Token inválido ou expirado' });
}
});
Mudanças críticas: expiração de 1 hora, validação real de assinatura com jwt.verify(), rate limiting, segredo forte em variável de ambiente.
Excessive Data Exposure — A3
Entendendo Overexposure de Dados
Excessive Data Exposure acontece quando a API retorna mais dados do que o cliente realmente precisa, ou quando retorna dados sensíveis que nunca deveriam ser expostos. Diferente de BOLA (onde você acessa dados de quem não deveria), aqui você tem permissão legítima mas a API expõe demais.
Exemplos: um endpoint de listagem de usuários que retorna senhas hash (why?), um endpoint de pedidos que retorna números de cartão de crédito em texto plano, ou um endpoint de perfil que retorna salários de todos os funcionários quando você só precisa do nome.
Exemplo Vulnerável: Retornando Dados Demais
// ❌ CÓDIGO VULNERÁVEL
app.get('/api/users/:userId', (req, res) => {
const user = database.getUser(req.params.userId);
// Retorna TUDO do banco de dados
res.json(user);
});
// Resposta:
// {
// "id": 123,
// "name": "João Silva",
// "email": "joao@example.com",
// "passwordHash": "$2b$10$...", ← Nunca deveria ser exposto
// "ssn": "123.456.789-00", ← Dado sensível desnecessário
// "internalNotes": "Problema de pagamento", ← Confidencial
// "salary": 5000, ← Não é da conta do cliente
// "lastLoginIP": "192.168.1.1" ← Informação desnecessária
// }
Implementação Segura
// ✅ CÓDIGO SEGURO
app.get('/api/users/:userId', (req, res) => {
const user = database.getUser(req.params.userId);
// Retorna APENAS o que é necessário
const safeUser = {
id: user.id,
name: user.name,
email: user.email,
// Não inclui: senhas, SSN, salários, notas internas
};
res.json(safeUser);
});
// Resposta segura:
// {
// "id": 123,
// "name": "João Silva",
// "email": "joao@example.com"
// }
Uma abordagem mais robusta é usar Data Transfer Objects (DTOs):
class UserDTO {
constructor(user) {
this.id = user.id;
this.name = user.name;
this.email = user.email;
// Apenas campos públicos, seguindo princípio da menor informação
}
}
app.get('/api/users/:userId', (req, res) => {
const user = database.getUser(req.params.userId);
res.json(new UserDTO(user));
});
Broken Function Level Authorization — A5
Conceito: Controle de Acesso por Funcionalidade
Enquanto BOLA trata de acesso a dados específicos, Broken Function Level Authorization é sobre acesso a funcionalidades inteiras. Um administrador tem certas ações que usuários normais não deveriam executar. Se a API não valida adequadamente, um usuário comum consegue executar ações administrativas.
Exemplos: um usuário comum conseguir deletar qualquer produto (função administrativa), um cliente conseguir alterar preços (função de gerenciador), ou qualquer pessoa conseguir exportar dados em lote (função de analista).
Exemplo Vulnerável: Falta de Validação de Papel
// ❌ CÓDIGO VULNERÁVEL
app.delete('/api/products/:productId', (req, res) => {
const { productId } = req.params;
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Não autenticado' });
}
// Apenas verifica autenticação, não o papel do usuário
database.deleteProduct(productId);
res.json({ message: 'Produto deletado' });
});
Um usuário "customer" autenticado consegue deletar produtos. Desastre.
Implementação Segura
// ✅ CÓDIGO SEGURO
// Middleware de autorização por papel
function requireRole(requiredRoles) {
return (req, res, next) => {
const user = req.user;
if (!user || !user.role) {
return res.status(403).json({ error: 'Acesso negado' });
}
if (!requiredRoles.includes(user.role)) {
return res.status(403).json({
error: `Requer papel(is): ${requiredRoles.join(', ')}`
});
}
next();
};
}
// Uso correto
app.delete('/api/products/:productId',
requireRole(['admin', 'manager']), // ← Validação explícita
(req, res) => {
const { productId } = req.params;
database.deleteProduct(productId);
res.json({ message: 'Produto deletado' });
}
);
// Endpoints públicos não usam middleware
app.get('/api/products', (req, res) => {
res.json(database.getAllProducts());
});
Injection Attacks em APIs — A4
Por Que APIs São Alvo de Injection
APIs processam dados estruturados (JSON, XML) em vez de formulários HTML, mas continuam vulneráveis a injeção. SQL Injection, NoSQL Injection, Command Injection — todas funcionam em APIs. A diferença é que em APIs o atacante pode estruturar payloads complexos em JSON, e muitos desenvolvedores não pensam em APIs como alvo de injection porque estão pensando em "input de formulário".
Injection em APIs é particularmente perigosa porque: (1) APIs frequentemente interagem diretamente com bancos de dados, (2) a resposta é estruturada em JSON, facilitando parseamento de resultados, (3) ferramentas de teste de API permitem craftar requisições complexas facilmente.
SQL Injection em API Node.js
// ❌ CÓDIGO VULNERÁVEL
app.get('/api/users/search', (req, res) => {
const { name } = req.query;
// Concatenação direta em SQL = VULNERÁVEL
const query = `SELECT * FROM users WHERE name = '${name}'`;
const results = database.query(query);
res.json(results);
});
// Requisição maliciosa:
// GET /api/users/search?name='; DROP TABLE users; --
// Resulta em: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
// ☠️ Deleta a tabela de usuários
Implementação Segura com Prepared Statements
// ✅ CÓDIGO SEGURO
const mysql = require('mysql2/promise');
app.get('/api/users/search', async (req, res) => {
const { name } = req.query;
try {
// Prepared statement - SQL e dados separados
const query = 'SELECT id, name, email FROM users WHERE name LIKE ?';
const [results] = await database.execute(query, [`%${name}%`]);
res.json(results);
} catch (error) {
res.status(500).json({ error: 'Erro ao buscar usuários' });
}
});
A diferença: ? é um placeholder. O banco de dados tratará o valor separadamente, impossibilitando injeção. O atacante não consegue "escapar" do contexto de dado.
NoSQL Injection
// ❌ CÓDIGO VULNERÁVEL (MongoDB)
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// Se o client envia {"username": {"$ne": ""}, "password": ...}
// A query se torna: {username: {$ne: ""}} que retorna qualquer usuário!
const user = db.collection('users').findOne({
username: username,
password: password
});
if (user) {
res.json({ token: generateToken(user) });
}
});
Validação Segura para NoSQL
// ✅ CÓDIGO SEGURO
const validator = require('validator');
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// Valida tipos - rejeita objetos quando string é esperada
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Credenciais inválidas' });
}
// Sanitização adicional
if (!validator.isLength(username, { min: 3, max: 50 })) {
return res.status(400).json({ error: 'Usuário inválido' });
}
const user = db.collection('users').findOne({
username: username, // Agora definitivamente uma string
password: hashPassword(password)
});
if (user) {
res.json({ token: generateToken(user) });
} else {
res.status(401).json({ error: 'Credenciais inválidas' });
}
});
Broken API Access Control — A6
Integração com Broken Function Level Authorization
Enquanto Broken Function Level Authorization trata de funções, Broken API Access Control é mais amplo: inclui falhas em políticas de acesso em geral. Inclui falta de controle de versão de API, endpoints não documentados ainda acessíveis, ausência de rate limiting, e falta de validação de origem (CORS).
Uma API pode ter autenticação e autorização perfeitas, mas se não controlar quantas requisições um usuário faz por minuto, um atacante consegue fazer força bruta ou DoS.
Exemplo: Falta de Rate Limiting
// ❌ CÓDIGO VULNERÁVEL
app.post('/api/send-reset-email', (req, res) => {
const { email } = req.body;
// Nada impede que alguém envie 10.000 requisições por segundo
sendResetEmail(email);
res.json({ message: 'Email enviado' });
});
// Um atacante pode:
// 1. Fazer força bruta em emails válidos
// 2. Causar DoS alagando o servidor
// 3. Enviar spam para qualquer email
Implementação Segura com Rate Limiting
// ✅ CÓDIGO SEGURO
const rateLimit = require('express-rate-limit');
// Limita 5 requisições por IP por 15 minutos
const resetLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Muitas tentativas de reset. Tente novamente em 15 minutos.',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/send-reset-email', resetLimiter, (req, res) => {
const { email } = req.body;
// Valida formato do email
if (!validator.isEmail(email)) {
return res.status(400).json({ error: 'Email inválido' });
}
sendResetEmail(email);
res.json({ message: 'Email enviado se o endereço existe' });
// Nota: Não revelamos se o email existe ou não (evita enumeração)
});
CORS Seguro
// ❌ CÓDIGO VULNERÁVEL
const cors = require('cors');
app.use(cors()); // Permite QUALQUER origem
// ✅ CÓDIGO SEGURO
const cors = require('cors');
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['https://app.exemplo.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Conclusão
Aprendemos que segurança de APIs não é apenas "colocar autenticação" e pronto. As três lições principais que deve internalizar são:
-
Autorização é diferente de Autenticação: Você pode estar autenticado (provei quem sou) mas não autorizado (não tenho permissão para esta ação). BOLA e Broken Function Level Authorization exploram exatamente isso. Sempre valide explicitamente se o usuário pode acessar aquele recurso ou executar aquela função.
-
Dados retornados devem ser mínimos: O princípio de menor privilégio também se aplica a dados. Retorne apenas o que o cliente realmente precisa. Use DTOs, whitelisting de campos, e nunca retorne senhas, tokens, ou dados internos na resposta.
-
Validação e Sanitização são sua primeira linha de defesa contra injection e DoS: Não confie no cliente. Valide tipos, tamanhos, formatos. Use prepared statements para queries, evite concatenação de strings, implemente rate limiting. Essas práticas simples previnem 80% das vulnerabilidades.