Volver al blog
Next.jsReactSeguridadSSRFull-Stack

Mejores prácticas de seguridad en Next.js: una guía completa para 2026

Introducción

Next.js 16 impulsa desde blogs estáticos hasta plataformas SaaS empresariales. Su modelo híbrido — server components, rutas de API, middleware y exportaciones estáticas — da a los desarrolladores una flexibilidad enorme. Pero esa misma flexibilidad crea una superficie de ataque más amplia que la de una aplicación de una sola página tradicional.

Una SPA de React tiene un único modelo de amenaza: el navegador. Next.js tiene tres: el servidor (rutas de API, server actions, SSR), la red (middleware, reglas de reescritura, cabeceras) y el cliente (hidratación, evasiones de CSP).

Esta guía cubre los patrones de seguridad que todo desarrollador de Next.js debería conocer en 2026. Cada sección incluye código listo para producción.


1. Política de seguridad de contenido con nonces

Una CSP estricta es tu defensa más fuerte contra el XSS, pero Next.js inyecta scripts en línea para sus datos de arranque (__NEXT_DATA__) y las precargas de segmentos de ruta. Sin un nonce, te ves obligado a usar 'unsafe-inline' — lo que anula el propósito.

Generación de nonces en el servidor

Genera un nonce criptográficamente aleatorio una vez por petición usando middleware:

typescript
// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(req: NextRequest) {
  const nonce = crypto.randomUUID();

  const csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' https: data: blob:`,
    `connect-src 'self' https://api.yoursite.com`,
    `frame-ancestors 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
  ].join("; ");

  const response = NextResponse.next();
  response.headers.set("Content-Security-Policy", csp);
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");

  // Pasa el nonce a la petición para que layout.tsx pueda usarlo
  response.headers.set("x-nonce", nonce);
  return response;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Aplicar el nonce en el layout

tsx
// src/app/layout.tsx
import { headers } from "next/headers";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const headersList = await headers();
  const nonce = headersList.get("x-nonce") ?? "";

  return (
    <html lang="en">
      <head>
        <script nonce={nonce}
          dangerouslySetInnerHTML={{
            __html: `window.__NONCE__ = '${nonce}';`,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Con 'strict-dynamic' en la CSP, cualquier script cargado por un script ya de confianza hereda esa confianza — eliminando la necesidad de poner nonce a cada etiqueta script en línea.


2. Endurecimiento de las rutas de API

Cada archivo de src/app/api/ es un endpoint HTTP de acceso público. Los errores comunes incluyen autenticación ausente, falta de limitación de velocidad y confiar en las cabeceras Content-Type.

Validación de entradas con Zod

Valida siempre los cuerpos de las peticiones antes de tocar tu base de datos:

typescript
// src/app/api/contact/route.ts
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const contactSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  message: z.string().min(10).max(5000),
  // Previene la contaminación de prototipos
  company: z.string().max(200).optional(),
});

export async function POST(req: NextRequest) {
  let body: unknown;
  try {
    body = await req.json();
  } catch {
    return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
  }

  const parsed = contactSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.flatten() },
      { status: 422 }
    );
  }
  // ... procesa parsed.data de forma segura
}

Limitación de velocidad mediante middleware

Usa el KV de Vercel o un simple mapa en memoria para despliegues más pequeños:

typescript
// src/lib/rate-limit.ts
const rateMap = new Map<string, { count: number; resetAt: number }>();

export function rateLimit(ip: string, limit = 20, window = 60_000): boolean {
  const now = Date.now();
  const entry = rateMap.get(ip);

  if (!entry || now > entry.resetAt) {
    rateMap.set(ip, { count: 1, resetAt: now + window });
    return true;
  }

  if (entry.count >= limit) return false;
  entry.count++;
  return true;
}

Aplícalo en tu middleware o dentro de cada manejador de rutas de API.


3. Server Actions y protección CSRF

Next.js 13+ introdujo las Server Actions: funciones que se ejecutan en el servidor pero que pueden llamarse desde los client components. Internamente usan POST e incluyen un token CSRF automáticamente, pero solo para peticiones del mismo origen.

Patrón seguro de Server Action

typescript
// src/app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { z } from "zod";
import { rateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";

const feedbackSchema = z.object({
  rating: z.number().min(1).max(5),
  comment: z.string().max(2000),
});

export async function submitFeedback(formData: FormData) {
  const headersList = await headers();
  const ip = headersList.get("x-forwarded-for") ?? "unknown";

  if (!rateLimit(ip, 10)) {
    return { error: "Too many requests" };
  }

  const parsed = feedbackSchema.safeParse({
    rating: Number(formData.get("rating")),
    comment: formData.get("comment"),
  });

  if (!parsed.success) {
    return { error: "Invalid input" };
  }

  // Guarda en la base de datos aquí...

  revalidatePath("/feedback");
  return { success: true };
}

Regla clave: nunca llames a Server Actions desde formularios de otros orígenes. Next.js incrusta un token CSRF en la página, pero solo las peticiones del mismo origen lo incluyen.


4. Obtención de datos: evitar riesgos de serialización

Cuando obtienes datos en Server Components, estás ejecutando código en el servidor. Una respuesta de API maliciosa no puede ejecutar código arbitrario, pero sí puede causar:

  • Ataques de asignación masiva (mass assignment) — fugando campos que no tenías intención de exponer
  • Contaminación de prototipos — si propagas (spread) los datos de la respuesta de forma insegura

Selección segura de datos

tsx
// src/app/page.tsx — Server Component
async function getData() {
  const res = await fetch("https://api.example.com/users");
  const users: unknown = await res.json();

  // Valida la forma antes de usarla
  if (!Array.isArray(users)) return [];

  return users.map((u) => ({
    id: String(u?.id ?? ""),
    name: String(u?.name ?? "Unknown"),
    // Omite explícitamente los campos sensibles
  }));
}

Nunca hagas esto:

tsx
// PELIGROSO: propaga toda la respuesta de la API en el cliente
const data = await getData();
return <ClientComponent data={data} />;
// Si la API devuelve `{ ...user, isAdmin: true }`, se fuga

En su lugar, da forma a los datos en el servidor antes de pasarlos a los client components. Usa esquemas Zod para validar las respuestas de APIs externas en el límite.


5. Higiene de las variables de entorno

Next.js difumina la línea entre servidor y cliente. Un prefijo NEXT_PUBLIC_ hace que una variable esté disponible en los bundles del navegador. Cualquier cosa sin ese prefijo permanece solo en el servidor.

| Prefijo | Disponible en | Ejemplo | |:-------|:-----------|:--------| | NEXT_PUBLIC_ | Cliente + Servidor | NEXT_PUBLIC_GA_ID | | (ninguno) | Solo servidor | DB_PASSWORD, API_SECRET |

Fuga común: propagación de objetos de process.env

tsx
// ¡Esto fuga TODAS las variables de entorno al cliente!
export default function Page() {
  return <pre>{JSON.stringify(process.env)}</pre>;
}

Patrón de protección

typescript
// src/env.ts
import { z } from "zod";

const serverEnv = z.object({
  DB_URL: z.string().url(),
  API_SECRET: z.string().min(1),
  NODE_ENV: z.enum(["development", "production", "test"]),
});

const clientEnv = z.object({
  NEXT_PUBLIC_GA_ID: z.string().optional(),
});

// Lanza un error en tiempo de build si falta una variable de entorno obligatoria
export const env = {
  ...serverEnv.parse(process.env),
  ...clientEnv.parse(process.env),
};

6. Exportación estática: la superficie de ataque más pequeña

Si tu aplicación no necesita servidor — sin rutas de API, sin Server Actions, sin SSR — expórtala como HTML estático:

typescript
// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
  trailingSlash: true,
};

Una exportación estática elimina clases enteras de ataques:

  • Sin RCE en el servidor mediante deserialización
  • Sin SSRF procedente de llamadas fetch en rutas de API
  • Sin CSRF en Server Actions
  • Sin evasiones de middleware

Sirve la salida estática (directorio out/) detrás de una CDN con caché agresiva. Tu única preocupación restante es el XSS del lado del cliente, del que se encarga la CSP.

Este enfoque impulsa este mismo sitio web (jssecurityaudit.com) — una exportación estática de Next.js servida mediante nginx.


7. Lista de verificación de seguridad para Next.js

Usa esta lista antes de cada despliegue de producción:

  • [ ] CSP con noncesscript-src 'nonce-{random}' (sin 'unsafe-inline')
  • [ ] Middleware — cabeceras CSP, limitación de velocidad y cabeceras de seguridad aplicadas globalmente
  • [ ] Rutas de API — validación Zod en todas las entradas, comprobaciones de autenticación, limitación de velocidad
  • [ ] Server Actions — funciones "use server" con limitación de velocidad y validadas
  • [ ] Sin fugas de NEXT_PUBLIC_ — comprobado con la protección env.ts en tiempo de build
  • [ ] Registro OTEL/auditoría — acceso a la API registrado en producción
  • [ ] Autorización en manejadores de ruta — nunca confíes en los roles del lado del cliente
  • [ ] rewrites/redirects revisados — sin redirecciones abiertas mediante rutas controladas por el usuario
  • [ ] Dependencias actualizadasnpm audit --audit-level=critical pasa
  • [ ] Exportación estática considerada — si no se necesita servidor, output: "export" es lo más seguro

Conclusión

La seguridad de Next.js no es una sola cosa: es una cadena de prácticas en todo el stack. Los nonces CSP bloquean el XSS. Los esquemas Zod validan cada límite de API. El middleware aplica límites de velocidad y cabeceras de seguridad. Y cada variable NEXT_PUBLIC_ es una decisión consciente.

El principio más importante: define claramente tus límites de confianza. Sabes qué se ejecuta en el servidor, qué llega al cliente y dónde cruzan los datos entre ambos. Cada punto de cruce es una vulnerabilidad potencial.

Si necesitas una evaluación profesional de tu aplicación Next.js, reserva una auditoría de seguridad. Revisaremos tu configuración de middleware, el endurecimiento de las rutas de API, tu política CSP y el flujo de datos — desde next start hasta producción.

Explora nuestra gama completa de servicios de auditoría de seguridad →

JS Security Audit — Auditorías de seguridad profesionales de React, Next.js y Node.js.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.