Lanzar una app Next.js con Cursor, v0 o Bolt es rápido, pero la velocidad esconde fallos que no ves hasta que alguien los encuentra por ti. Esta checklist repasa lo que hay que revisar antes de pulsar deploy, con el porqué de cada punto, cómo comprobarlo y el código para arreglarlo. Al final tienes la versión de dos minutos.
Cómo piensa Next.js la frontera entre servidor y cliente
Casi todos los fallos de seguridad en Next.js son el mismo fallo: algo que debía quedarse en el servidor cruzó al navegador. Los Server Components se ejecutan en el servidor y nunca envían su código al cliente; en cuanto un archivo lleva 'use client', todo lo que importa acaba en el bundle público. Los Route Handlers (app/api/*/route.ts) y las Server Actions son URLs públicas aunque no las veas. El middleware corre antes que todo, pero no es una muralla. Si tienes clara esa frontera, el resto de la lista es mecánica.
1. Variables de entorno: cuidado con NEXT_PUBLIC
Por qué importa. Todo lo que empieza por NEXT_PUBLIC_ se incrusta en el JavaScript del navegador en el momento del build. Cualquiera puede leerlo con las herramientas de desarrollador. Nunca pongas ahí una clave de API privada, un token de servicio ni el secreto de tu base de datos.
Cómo comprobarlo. Abre tu .env y tu panel de Vercel y lee cada variable con prefijo público preguntándote: ¿pasaría algo si la publico en Twitter? Si la respuesta es sí, quítale el prefijo y muévela al servidor.
Cómo arreglarlo. Las variables sin prefijo solo existen en el servidor: léelas desde Server Components, Route Handlers o Server Actions. Para que un módulo con secretos nunca pueda importarse desde un componente de cliente, añade import 'server-only' en su primera línea: si alguien lo importa desde el lado equivocado, el build falla en vez de filtrar la clave. Más detalle en los secretos que tu app filtra en el navegador.
2. Route Handlers y Server Actions: valida y autoriza siempre
Por qué importa. Una ruta de API no es privada por estar en tu carpeta. Es una URL pública que cualquiera puede llamar con curl. Y una Server Action, aunque se invoque desde un botón, también: Next.js la expone como un endpoint POST con un identificador. Que un botón no aparezca para un usuario no impide que alguien golpee el endpoint directamente.
Cómo arreglarlo. Comprueba la sesión y los permisos dentro de cada handler y de cada action, no solo en la interfaz. Valida el cuerpo de la petición con un esquema (zod o similar) antes de tocar la base de datos. Nunca aceptes el identificador del usuario desde el cliente: sácalo de la sesión. Y pon un límite de peticiones a lo que cueste dinero o envíe correos.
// app/actions/update-profile.ts
'use server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
const Schema = z.object({ name: z.string().min(1).max(80) })
export async function updateProfile(input: unknown) {
const session = await getSession()
if (!session) throw new Error('Unauthorized')
const data = Schema.parse(input)
// usa session.user.id, nunca un id que venga del cliente
await db.user.update({ where: { id: session.user.id }, data })
}
3. El middleware no es tu única autenticación
Por qué importa. Es tentador proteger todo /dashboard desde middleware.ts y olvidarse. En marzo de 2025 se publicó CVE-2025-29927: una cabecera interna (x-middleware-subrequest) permitía saltarse el middleware por completo en las versiones afectadas, y con él cualquier comprobación de sesión que viviera solo ahí. Se corrigió en Next.js 15.2.3 y 14.2.25 (con parches para 13 y 12), pero la lección queda.
Cómo arreglarlo. Actualiza Next.js a una versión parcheada y usa el middleware como primera capa (redirigir a quien no tiene sesión), nunca como la única. La comprobación que de verdad protege los datos va en la página, en el handler o en la action que los sirve.
4. Cabeceras de seguridad
Por qué importa. Por defecto Next.js no envía cabeceras defensivas. Sin ellas tu app queda expuesta a clickjacking, a que el navegador ejecute como script lo que no lo es y a degradación a HTTP.
Cómo arreglarlo. En next.config.js:
// next.config.js
const securityHeaders = [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]
module.exports = {
poweredByHeader: false,
productionBrowserSourceMaps: false,
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
},
}
La Content-Security-Policy merece aparte: Next.js inyecta scripts inline para hidratar la página, así que una CSP estricta necesita un nonce generado en el middleware y pasado a los componentes. Empieza en modo Report-Only. El detalle de cada cabecera está en las cabeceras de seguridad explicadas para humanos.
5. Secretos que cruzan a Client Components
Por qué importa. Es fácil que una clave privada acabe en un componente de cliente por error, sobre todo cuando la IA reescribe archivos y mueve imports. Si aparece en el JavaScript servido, ya está comprometida.
Cómo comprobarlo. Tras next build, busca en .next/static cadenas como sk_live, service_role, sk-ant o URLs de webhooks. Si aparecen, rótalas y muévelas al servidor con import 'server-only' como seguro.
6. Imágenes remotas y peticiones a URLs del usuario
Por qué importa. next/image optimiza imágenes a través de tu servidor. Si en images.remotePatterns permites cualquier host, cualquiera puede usar tu servidor como proxy de imágenes y gastar tu cuota. Lo mismo con una ruta de API que descarga la URL que le pasa el usuario: puede apuntar a servicios internos de tu infraestructura (SSRF).
Cómo arreglarlo. Limita remotePatterns a los dominios que de verdad usas. Si una ruta tiene que descargar URLs externas, valida el protocolo (solo https), rechaza IPs privadas y nombres internos, y pon un tiempo máximo.
7. Redirecciones abiertas
Por qué importa. El clásico redirect(searchParams.next) tras el login permite construir enlaces de tu dominio que acaban en una web de phishing: tudominio.com/login?next=https://malo.example.
Cómo arreglarlo. Acepta solo rutas relativas que empiecen por / (y no por //), o compara contra una lista de destinos permitidos.
8. Archivos que no deberían estar accesibles
Por qué importa. Todo lo que hay en public/ se sirve tal cual: un .env copiado ahí por error, un volcado de base de datos o un service-account.json quedan a una URL de distancia. Y los source maps en producción permiten leer tu código de cliente con nombres originales.
Cómo comprobarlo. Prueba tudominio.com/.env, /.git/HEAD y abre un archivo JS del bundle para ver si termina en una línea sourceMappingURL que apunte a un .map accesible. Revisa la carpeta public/ con los ojos de un desconocido.
9. Cookies de sesión y librerías de autenticación
Por qué importa. Auth.js (NextAuth), Clerk o Supabase con su paquete para servidor configuran bien las cookies por defecto. Lo que falla es lo que añades tú: una cookie propia sin HttpOnly, un AUTH_SECRET débil o copiado de un tutorial, o la sesión guardada en localStorage.
Cómo arreglarlo. Genera el secreto con openssl rand -base64 32, guárdalo sin prefijo público, y para cualquier cookie propia usa Secure, HttpOnly y SameSite=Lax.
10. Dependencias y la versión de Next.js
Por qué importa. Tu app arrastra cientos de paquetes, y el propio Next.js publica correcciones de seguridad varias veces al año. El caso del middleware de 2025 afectó a quien no actualizaba.
Cómo arreglarlo. npm audit antes de cada despliegue, Dependabot o Renovate en el repositorio, y una regla sencilla: actualizar Next.js en el mismo mes en que sale un parche de seguridad.
Despliegue en Vercel: tres detalles
- Entornos. Las variables tienen ámbito (Production, Preview, Development). No des a los despliegues de preview las credenciales de producción: un preview es una URL pública con tu código más reciente.
- Protección de previews. Activa Deployment Protection para que las URLs de preview exijan login de Vercel; si no, cualquiera con el enlace ve la rama en curso.
- Cabeceras. Las de
next.config.jsse aplican en Vercel sin más; si además usasvercel.json, no dupliques.
Tu checklist de 2 minutos
- Ninguna variable
NEXT_PUBLIC_contiene un secreto; los módulos con claves llevanimport 'server-only'. - Cada Route Handler y Server Action comprueba la sesión y valida la entrada; el id de usuario sale de la sesión.
- Next.js en una versión parcheada; el middleware no es la única capa de auth.
- Cabeceras de seguridad en
next.config.js;poweredByHeader: false; CSP al menos en modo informe. - Bundle sin
sk_live,service_roleni webhooks; source maps desactivados. remotePatternsacotados; nada descarga URLs del usuario sin validar.- Redirecciones solo a rutas relativas.
public/sin archivos sensibles;.envy.gitinaccesibles.- Cookies con Secure, HttpOnly y SameSite;
AUTH_SECRETaleatorio. npm auditlimpio; previews protegidos y sin secretos de producción.
Comprueba todo esto en 30 segundos
Repasar esta lista a mano en cada deploy es tedioso y se olvida. Scanaris hace comprobaciones de solo lectura sobre tu sitio publicado: cabeceras, secretos expuestos en el JavaScript, archivos sensibles accesibles, source maps, cookies y CORS. Cada hallazgo llega con un prompt listo para pegar en Cursor o en v0 con el arreglo exacto para Next.js. Si lo dejas en monitorización, te avisa cuando una sesión de vibe coding vuelve a abrir algo. Para profundizar en tu stack, está la página de seguridad para Next.js y la checklist general de lanzamiento.
Preguntas frecuentes
¿Las Server Actions son seguras por defecto?
Son más cómodas que una ruta de API, no más seguras. Next.js las expone como endpoints públicos; la autenticación y la validación las pones tú dentro de cada una.
¿El middleware protege mis rutas?
Sirve para redirigir y para comprobaciones baratas, pero no debe ser la única barrera. Después del caso de 2025, la regla es comprobar la sesión también donde se sirven los datos.
¿Necesito CSP si uso Next.js?
Sí, pero con nonce, y empezando en modo informe. Sin nonce, una CSP estricta bloquea los scripts inline del propio framework y la página queda en blanco.