Next.js
Seguridad en Next.js
Explica las fronteras de seguridad de Next.js y cómo proteger inputs, sesiones, Actions, APIs, caché, uploads, webhooks, secretos y multi-tenancy.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica las fronteras de seguridad de Next.js y cómo proteger inputs, sesiones, Actions, APIs, caché, uploads, webhooks, secretos y multi-tenancy.
Next.js mueve trabajo al servidor, pero no elimina amenazas. El navegador, Proxy, Server Components, Actions, Route Handlers, cachés, base de datos, uploads y servicios externos son fronteras distintas. Todo dato que cruza una frontera debe validarse, autorizarse, minimizarse y tratarse bajo una política explícita.
attacker controls
├─ URL, params and search params
├─ headers, cookies and body
├─ files and external URLs
├─ direct calls to Actions/Handlers
└─ timing and concurrent requests
↓
server validates identity, input and authority
↓
least-privilege data access and safe outputCódigo server-only oculta implementación y secretos, pero la operación sigue expuesta mediante una request.
browser
→ CDN/WAF
→ Proxy
→ page/Action/Route Handler
→ DAL/service
→ database
→ external providerNo confíes en una capa porque “viene de tu app”. El cliente puede reproducir una request sin la UI. Headers pueden falsificarse si la infraestructura no los sobrescribe.
Valida por frontera:
const parsed = schema.safeParse({
orderId,
quantity: formData.get("quantity"),
});
if (!parsed.success) return { status: "invalid", errors: parsed.error.flatten() };La validación comprueba forma y límites. No autoriza. Un UUID válido todavía puede pertenecer a otro tenant.
Limita:
Cada Action/Handler sensible:
verify session
→ derive tenant and principal
→ load accessible resource
→ authorize action and current state
→ mutate atomicallyProxy y UI son filtros optimistas. Nunca aceptes userId, role, organizationId o precio como autoridad del cliente.
React escapa texto, pero existen salidas peligrosas:
dangerouslySetInnerHTML.javascript:.Sanitiza HTML con una librería mantenida y una allowlist apropiada. Serializa JSON-LD con JSON.stringify(...).replace(/</g, "\\u003c").
CSP reduce impacto, pero no corrige una inyección.
Cookies se adjuntan automáticamente. Defensas:
SameSite.Origin/Host en mutaciones.CORS no es protección CSRF completa y no impide llamadas servidor-a-servidor.
Usa ORM/queries parametrizadas:
await db.order.findMany({ where: { status: parsedStatus } });No interpoles nombres de columnas desde URL. Mapea allowlists.
Evita pasar input a shell commands. Si necesitas ejecutar una herramienta, usa argumentos separados, allowlist y sandbox.
Una función servidor que descarga una URL controlada puede alcanzar servicios internos:
user URL
→ server fetch
→ metadata service / localhost / private networkMitiga:
remotePatterns amplias y generadores OG también merecen revisión.
redirect(String(formData.get("next")));Permite phishing. Acepta solo rutas internas que comiencen con / pero no //, o hosts externos de allowlist.
Nunca construyas paths con nombres de usuario:
../../secretsGenera IDs propios, normaliza extensiones y almacena fuera del árbol ejecutable. En object storage, usa keys controladas y URLs firmadas.
Valida:
Para archivos grandes, upload directo firmado evita cargar toda la función. No sirvas HTML/SVG subido bajo tu origen sin headers y sanitización.
import "server-only";Evita imports servidor desde cliente, pero no impide que serialices el secreto. Usa DTOs mínimos.
Nunca prefijes secretos con NEXT_PUBLIC_, los pongas en props, logs o mensajes de error.
Rota credenciales y aplica least privilege por entorno.
Peligro:
async function getDashboard() {
"use cache";
const session = await getSession();
return db.dashboard.findMany({ where: { userId: session.userId } });
}Una caché pública o key incompleta puede compartir datos. Mantén request context fuera, incluye variante segura o evita cachear información personal.
Prueba dos usuarios y dos tenants con cache caliente.
Toda consulta privada incorpora tenant en la condición, no solo después:
await db.project.findFirst({
where: { id: projectId, organizationId: session.organizationId },
});Prueba escalación horizontal y vertical. Constraints y RLS pueden añadir defensa, no reemplazan la política de acción.
No parsees/modifiques el body antes de verificar si la firma usa bytes exactos.
Protege login, recuperación, uploads y APIs costosas. Un contador en memoria local no es confiable en múltiples instancias. Usa gateway/store compartido.
Combina actor/API key e IP confiable, evitando bloquear permanentemente una cuenta mediante ataques de terceros.
Un script de analytics ejecuta con acceso al DOM; evalúa cada tercero.
CSP, HSTS, X-Content-Type-Options, Referrer Policy, Permissions Policy y frame-ancestors son defensa en profundidad. Deben probarse y tener scope correcto. HSTS/preload es difícil de revertir.
No expongas stack, query, path interno o provider response. Devuelve un error estable con request ID.
Logs usan allowlist; nunca cookies, Authorization o passwords.
"use server";
export async function updateOrder(formData: FormData) {
const session = await requireSession();
const input = updateOrderSchema.parse(Object.fromEntries(formData));
const result = await orderService.updateAuthorized({
actor: session,
orderId: input.orderId,
expectedVersion: input.version,
patch: input.patch,
});
if (result.status === "success") updateTag(`order:${input.orderId}`);
return result;
}La Action vuelve a autenticar, no acepta tenant/role, usa versionado y solo invalida después del commit.
Automatiza SAST/dependency scanning, pero complementa con threat modeling y pruebas de integración.
La request sigue siendo controlable.
Un ID válido puede ser ajeno.
No protege endpoints.
XSS persistente.
SSRF.
Fuga entre usuarios.
El cliente lo falsifica.
Amplía impacto de un PR.
Content Security Policy y security headers profundiza en mitigaciones HTTP del navegador.