Pular para o conteúdo
segurança

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.

Douglas M. Pereira5 min de leitura
segurança apiowaspsql injectionautenticaçãorate limitingcorsnodejs

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