← Blog
·10 min read

Seguridad en Next.js — Guía Completa 2026

Guía completa de seguridad para Next.js 2026: Server Actions, middleware, API routes, variables de entorno, headers HTTP y patrones de autenticación. Con ejemplos reales.

Rod

Founder & Developer

La seguridad en Next.js tiene trampas que no son obvias. El App Router cambió cómo funciona el rendering, los Server Actions son un vector de ataque nuevo, y las variables de entorno tienen reglas que la IA no siempre respeta. Esta guía cubre los errores más comunes en proyectos Next.js reales — con el código exacto para arreglarlos.

Al escanear repos Next.js con Data Hogo, los mismos cinco problemas aparecen una y otra vez. Los repasamos todos.


Variables de entorno: NEXT_PUBLIC_ no significa "solo para el frontend"

El error más frecuente y más peligroso. NEXT_PUBLIC_ significa que esa variable va a estar en el bundle de JavaScript que el navegador descarga. Cualquier persona puede verla.

# MAL: estas keys van a estar en el HTML que el usuario ve
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE=eyJhbGci...
 
# BIEN: secretos solo en variables sin prefijo NEXT_PUBLIC_
STRIPE_SECRET_KEY=sk_live_abc123
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
 
# Correcto: solo el anon key va en NEXT_PUBLIC_
NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...

La regla es simple: si necesitas la variable en código que corre en el servidor (API routes, Server Actions, funciones de Node), va sin el prefijo. Si la necesitas en el cliente (componentes React, hooks), va con NEXT_PUBLIC_ — y eso significa que puede ser pública.

Verifica si tu .env está accesible públicamente desde tu URL de producción con la herramienta de escaneo de env expuesto.


API routes: la autenticación es tu responsabilidad

Next.js no protege tus API routes automáticamente. Cada endpoint que creas es accesible para cualquier persona que conozca la URL. La verificación de autenticación es tu responsabilidad — no del framework.

// MAL: cualquiera puede obtener todos los datos de usuarios
// src/app/api/users/route.ts
export async function GET(req: Request) {
  const users = await db.users.findMany();
  return Response.json(users);
}
 
// BIEN: verifica la sesión antes de devolver cualquier dato
// src/app/api/users/route.ts
import { createClient } from "@/lib/supabase/server";
 
export async function GET(req: Request) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
 
  if (!user) {
    return Response.json({ error: "No autorizado" }, { status: 401 });
  }
 
  // Solo los datos del usuario autenticado
  const userData = await db.users.findUnique({ where: { id: user.id } });
  return Response.json(userData);
}

El patrón correcto: la primera línea de cada handler verifica la sesión. Si no hay sesión válida, retorna 401 inmediatamente. Nada más corre.

Para entender bien por qué esto importa, el artículo de control de acceso roto en OWASP explica el patrón de ataque con ejemplos.


Server Actions: son endpoints HTTP, no magia de React

Este es el error nuevo que trajo el App Router. Los Server Actions parecen funciones de TypeScript normales, pero son endpoints HTTP reales que cualquiera puede llamar directamente. Si no verificas auth dentro del Server Action, queda expuesto.

// MAL: Server Action sin verificación de autenticación
"use server";
 
export async function deleteUser(userId: string) {
  // Cualquiera puede llamar esto con un fetch() directamente
  await db.users.delete({ where: { id: userId } });
}
 
// BIEN: verifica sesión y valida que el usuario puede hacer esa acción
"use server";
 
import { createClient } from "@/lib/supabase/server";
import { z } from "zod";
 
const schema = z.object({ userId: z.string().uuid() });
 
export async function deleteUser(userId: string) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
 
  if (!user) throw new Error("No autorizado");
 
  const { userId: validId } = schema.parse({ userId });
 
  // Verifica que el usuario puede borrar ese recurso
  if (user.id !== validId && !user.role === "admin") {
    throw new Error("Permisos insuficientes");
  }
 
  await db.users.delete({ where: { id: validId } });
}

Dos cosas son necesarias en cada Server Action: verificar la sesión y validar el input con Zod o similar. Nunca confíes en que el cliente mandó los datos correctos.


Middleware: qué protege y qué no

El middleware de Next.js es el lugar correcto para proteger rutas completas. Pero tiene una trampa: si usas el middleware solo como "portero" de la UI, sin verificar auth en los API routes, la protección no sirve de nada.

// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  const token = request.cookies.get("auth-token");
 
  // Rutas que requieren autenticación
  const protectedPaths = ["/dashboard", "/settings", "/api/user"];
  const isProtected = protectedPaths.some(path =>
    request.nextUrl.pathname.startsWith(path)
  );
 
  if (isProtected && !token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Lo que el middleware no puede hacer: verificar si el token es válido de verdad (no tiene acceso a la base de datos en Edge). Por eso la verificación real de la sesión siempre va dentro del handler — el middleware solo hace el redirect inicial para mejorar UX.


Security headers en Next.js

Los security headers son instrucciones que tu servidor manda al navegador para activar protecciones adicionales. Next.js no los configura por defecto.

// next.config.ts
import type { NextConfig } from "next";
 
const securityHeaders = [
  // Evita que el navegador "adivine" el tipo de archivo
  { key: "X-Content-Type-Options", value: "nosniff" },
  // Evita que tu app se muestre en un iframe (clickjacking)
  { key: "X-Frame-Options", value: "DENY" },
  // Activa HTTPS estrictamente por 2 años
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload"
  },
  // Controla desde dónde se puede cargar contenido
  {
    key: "Content-Security-Policy",
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
    ].join("; ")
  },
  // Controla cuánta información del referrer se comparte
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
];
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};
 
export default nextConfig;

Después de configurarlos, verifica que estén activos en producción. Vercel a veces tiene configuraciones que sobreescriben headers. Usa el verificador de cabeceras de seguridad gratis para confirmarlo.


Validación de input: nunca confíes en el cliente

Cualquier dato que venga del cliente — body de un POST, query params, headers — puede ser manipulado. La IA genera handlers que asumen que el input es válido. Eso se explota fácil.

// MAL: confía en que el body tiene el formato correcto
export async function POST(req: Request) {
  const { email, amount } = await req.json();
  await processPayment(email, amount); // ¿Y si amount es negativo?
}
 
// BIEN: valida con Zod antes de tocar la lógica de negocio
import { z } from "zod";
 
const paymentSchema = z.object({
  email: z.string().email(),
  amount: z.number().positive().max(10000), // Límite razonable
});
 
export async function POST(req: Request) {
  const body = await req.json();
  const result = paymentSchema.safeParse(body);
 
  if (!result.success) {
    return Response.json(
      { error: "Datos inválidos", details: result.error.flatten() },
      { status: 400 }
    );
  }
 
  await processPayment(result.data.email, result.data.amount);
}

Zod valida el tipo, el formato y las restricciones de negocio en una sola llamada. La instalación toma un minuto. El tiempo que ahorra en debugging y ataques, mucho más.


Escanea tu app Next.js antes del deploy

Todos los patrones de esta guía — variables de entorno mal configuradas, API routes sin auth, headers faltantes — aparecen al escanear repos Next.js reales. No como casos raros. Como el promedio.

La forma más rápida de saber si tu app tiene estos problemas es un escaneo automatizado. Data Hogo analiza tu repo en menos de 5 minutos y devuelve un security score con cada hallazgo explicado en español. El plan gratuito cubre 3 scans al mes.

Escanea tu repo Next.js gratis →


Preguntas frecuentes

¿Qué vulnerabilidades de seguridad son más comunes en Next.js?

Las más frecuentes son: API routes sin verificación de autenticación, secretos expuestos en variables de entorno públicas (NEXT_PUBLIC_), Server Actions sin validación de input, security headers HTTP faltantes, y CSRF en formularios. Al escanear repos Next.js encontramos al menos una de estas en la mayoría de proyectos.

¿Cómo protejo mis Server Actions en Next.js?

Verifica la sesión del usuario al inicio de cada Server Action, valida todos los inputs con Zod o una librería similar, y nunca asumas que el cliente mandó datos correctos. Los Server Actions son endpoints HTTP reales — cualquiera puede llamarlos directamente con un curl si no verificas auth.

¿Cómo configuro los security headers en Next.js?

En next.config.ts, usa la opción headers() para agregar X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security y Content-Security-Policy. Verifica que estén activos en producción con el verificador de headers de Data Hogo — Vercel a veces sobreescribe configuraciones.

¿Las variables NEXT_PUBLIC_ en Next.js son seguras?

Las variables con prefijo NEXT_PUBLIC_ se incluyen en el bundle del cliente — son visibles para cualquier persona que inspeccione tu JavaScript en producción. Nunca pongas API keys privadas, tokens de servicio ni secretos en variables NEXT_PUBLIC_. Esas solo van en variables sin el prefijo, que solo son accesibles en código server-side.

¿Cómo sé si mi app Next.js tiene vulnerabilidades de seguridad?

La forma más rápida es un escaneo automatizado. Data Hogo analiza tu repo Next.js en menos de 5 minutos y detecta secretos expuestos, patrones de código inseguros, dependencias vulnerables y headers faltantes — con explicaciones en español de cómo arreglar cada uno.

nextjsseguridadserver actionsapi routesmiddlewarevariables de entornotypescript