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-limitimport 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 zodimport { 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
dangerouslySetInnerHTMLcom 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
- LGPD para Devs — Proteção de dados pessoais
- Autenticação Segura — Login sem erros
- Variáveis de Ambiente Seguras — Gerenciar secrets