guias
Segurança

Proteção de API

Rate limiting, validação de input e proteção contra ataques comuns.

Sua API está exposta na internet. Sem proteção, qualquer pessoa pode abusar dela. Este guia cobre as defesas essenciais.

Rate Limiting

Limitar quantas requisições um usuário pode fazer por minuto:

Next.js (App Router)

npm install @upstash/ratelimit @upstash/redis
// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
 
export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "60 s"), // 10 requests por minuto
});
// app/api/users/route.ts
import { ratelimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
 
export async function POST(req: Request) {
  const headersList = await headers();
  const ip = headersList.get("x-forwarded-for") ?? "anonymous";
 
  const { success } = await ratelimit.limit(ip);
  if (!success) {
    return Response.json(
      { error: "Muitas requisições. Tente novamente em 1 minuto." },
      { status: 429 },
    );
  }
 
  // Processar requisição...
}
veloz env set UPSTASH_REDIS_REST_URL=https://...
veloz env set UPSTASH_REDIS_REST_TOKEN=...

Express/Hono (simples, sem Redis)

npm install express-rate-limit
import rateLimit from "express-rate-limit";
 
const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minuto
  max: 30, // 30 requisições por minuto
  message: { error: "Muitas requisições. Tente novamente em breve." },
});
 
app.use("/api/", limiter);

Validação de Input

Nunca confie nos dados que chegam do cliente:

Com Zod (recomendado)

npm install zod
import { z } from "zod";
 
const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(18).max(120).optional(),
});
 
export async function POST(req: Request) {
  const body = await req.json();
 
  const result = createUserSchema.safeParse(body);
  if (!result.success) {
    return Response.json(
      { error: "Dados inválidos", details: result.error.flatten() },
      { status: 400 },
    );
  }
 
  // result.data é tipado e validado
  const user = await db.user.create({ data: result.data });
  return Response.json(user, { status: 201 });
}

Erros comuns de validação

// ❌ Sem validação — aceita qualquer coisa
const { name, email } = await req.json();
await db.user.create({ data: { name, email } });
 
// ❌ Validação fraca
if (!name) return Response.json({ error: "Nome obrigatório" });
// E se name for um número? Um objeto? Uma string de 10MB?
 
// ✅ Validação com Zod
const data = createUserSchema.parse(await req.json());

SQL Injection

Se você usa ORMs como Prisma ou Drizzle, já está protegido. Mas se faz queries manuais:

// ❌ Vulnerável a SQL Injection
const result = await sql`SELECT * FROM users WHERE email = '${email}'`;
 
// ✅ Parameterized query
const result = await sql`SELECT * FROM users WHERE email = ${email}`;

A diferença é sutil mas crítica: sem aspas ao redor de ${email}, a maioria dos drivers trata como parâmetro seguro.

Regra: Use sempre ORMs (Prisma, Drizzle) ou parameterized queries. Nunca concatene strings em SQL.

XSS (Cross-Site Scripting)

Prevenir que usuários injetem JavaScript malicioso:

// ❌ Renderizar HTML do usuário diretamente
<div dangerouslySetInnerHTML={{ __html: userInput }} />
 
// ✅ React já escapa por padrão
<div>{userInput}</div>
 
// ✅ Se precisar de HTML, sanitize primeiro
import DOMPurify from "dompurify"
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

Regra: Nunca use dangerouslySetInnerHTML com dados de usuários sem sanitizar.

CORS

Controlar quais domínios podem acessar sua API:

// ❌ Aceitar qualquer origem
app.use(cors()); // Perigoso em produção
 
// ✅ Especificar origens permitidas
app.use(
  cors({
    origin: ["https://meuapp.com", "https://www.meuapp.com"],
    credentials: true,
  }),
);

Next.js

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/api/:path*",
        headers: [
          { key: "Access-Control-Allow-Origin", value: "https://meuapp.com" },
          { key: "Access-Control-Allow-Methods", value: "GET,POST,PUT,DELETE" },
          { key: "Access-Control-Allow-Headers", value: "Content-Type,Authorization" },
        ],
      },
    ];
  },
};

CSRF Protection

Para apps com cookies de sessão:

// Gerar token CSRF
import { randomBytes } from "crypto";
const csrfToken = randomBytes(32).toString("hex");
 
// Verificar no server
if (req.headers["x-csrf-token"] !== session.csrfToken) {
  return Response.json({ error: "Token CSRF inválido" }, { status: 403 });
}

Dica: Se você usa Bearer tokens (JWT) no header Authorization, CSRF não é um problema — apenas cookies são vulneráveis.

Webhook Verification

Se seu app recebe webhooks (Stripe, GitHub, etc.), sempre verifique a assinatura:

Stripe

import Stripe from "stripe";
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;
 
  try {
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
    // Processar evento...
  } catch (err) {
    return Response.json({ error: "Assinatura inválida" }, { status: 400 });
  }
}
veloz env set STRIPE_WEBHOOK_SECRET=whsec_...

Checklist

  • Rate limiting em todas as rotas públicas
  • Validação de input com Zod (ou similar)
  • ORM ou parameterized queries (nunca concatenar SQL)
  • Sem dangerouslySetInnerHTML com dados de usuários
  • CORS configurado com origens específicas
  • Webhooks com verificação de assinatura
  • Headers de segurança (X-Frame-Options, CSP, etc.)

Próximos passos