Las Server Actions de Next.js exponen automáticamente cada función exportada en un archivo use-server como un endpoint HTTP público, asignándole un identificador único que el cliente utiliza para invocar la función. El endpoint existe independientemente de si se renderiza un botón, un enlace o cualquier otro elemento de la interfaz de usuario, lo que significa que cualquier persona que descubra el identificador puede llamar a la función directamente.
El diseño del framework trata a las Server Actions como funciones auxiliares ordinarias, pero en tiempo de ejecución se convierten en URLs accesibles. Para los desarrolladores que asumían que la interfaz de usuario era el único guardián, esto crea una superficie de ataque invisible que puede ser explotada con una sola solicitud manipulada.
Por qué las Server Actions parecían seguras
Las Server Actions se introdujeron para permitir que los desarrolladores escriban código del lado del servidor junto a sus componentes, evitando el código repetitivo (boilerplate) de las rutas de API separadas. Un uso típico se ve así:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
Debido a que el formulario es la única forma visible de activar deleteInvoice, muchos desarrolladores ocultan el botón para usuarios no autorizados, piensan que las firmas de TypeScript detendrán los datos malformados y confían en el hecho de que la función reside en un módulo exclusivo del servidor. Ninguna de esas suposiciones proporciona una protección real.
La exposición oculta
Cuando un archivo contiene “use server”, Next.js compila cada función exportada en un endpoint como este:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
El <action-id> es un hash estable que el bundle del cliente incorpora. Un atacante puede obtenerlo mediante:
- Inspeccionando el tráfico de red de la página.
- Leyendo el JavaScript empaquetado (el ID es una cadena de texto simple).
- Adivinando basándose en convenciones de nomenclatura si el proyecto sigue patrones predecibles.
Una vez que se conoce el ID, se puede enviar una solicitud desde cualquier herramienta —cURL, Postman o un script malicioso— eludiendo cualquier comprobación a nivel de interfaz de usuario.
Tres riesgos concretos
| Riesgo | Por qué es importante |
|---|---|
| Las comprobaciones de la interfaz de usuario son ineficaces | Ocultar un botón o un enlace no elimina el endpoint subyacente. El endpoint sigue siendo accesible, al igual que una página de administración oculta que aún existe en el servidor. |
| TypeScript no ofrece seguridad en tiempo de ejecución | Los tipos se eliminan cuando se ejecuta el código. Una función declarada como deleteInvoice(id: number) puede recibir una cadena masiva, un array o incluso un JSON malicioso, lo que provoca errores de lógica o ataques de inyección. |
| Sin autenticación ni autorización por defecto | Las Server Actions parecen funciones auxiliares locales, por lo que los desarrolladores suelen olvidar añadir comprobaciones de sesión, protección CSRF o comprobaciones de permisos a nivel de fila que son estándar en las rutas de API tradicionales. |
Cómo asegurar una Server Action
- Autenticar al llamador – Verificar que exista una sesión o un token válido antes de que se ejecute cualquier lógica de negocio.
- Validar la carga útil (payload) – Utilizar una librería de esquemas (por ejemplo, Zod, Yup) para imponer tipos de datos y restricciones de valores en tiempo de ejecución.
- Autorizar la operación – Más allá de "¿está el usuario conectado?", confirmar que el usuario es el propietario del registro específico que intenta modificar o eliminar.
Un ejemplo mínimo:
'use server';
import { getSession } from '@/auth';
import { z } from 'zod';
import { db } from '@/db';
const DeleteInvoiceSchema = z.object({
id: z.number().int().positive(),
});
export async function deleteInvoice(formData: FormData) {
const session = await getSession();
if (!session) throw new Error('Unauthenticated');
const parsed = DeleteInvoiceSchema.safeParse({
id: Number(formData.get('id')),
});
if (!parsed.success) throw new Error('Invalid input');
const invoice = await db.invoice.findUnique({ where: { id: parsed.data.id } });
if (!invoice || invoice.ownerId !== session.userId) {
throw new Error('Unauthorized');
}
await db.invoice.delete({ where: { id: invoice.id } });
}
El código comprueba explícitamente la autenticación, valida el id entrante y garantiza que el usuario conectado sea realmente el propietario de la factura antes de realizar la eliminación.
Otros escollos sutiles de Next.js
| Problema | Síntoma | Solución |
|---|---|---|
Variables de entorno NEXT_PUBLIC_ |
Cualquier cosa con el prefijo NEXT_PUBLIC_ se empaqueta en el cliente, exponiendo secretos. |
Mantén los secretos en variables de entorno normales, nunca les pongas el prefijo NEXT_PUBLIC_. |
| Redirecciones abiertas | Aceptar un parámetro de consulta redirect y concatenarlo ingenuamente puede enviar a los usuarios a //evil.com. |
Valida el destino contra una lista blanca o impón comprobaciones de mismo origen (same-origin). |
| Server-Side Request Forgery (SSRF) | Obtener una URL proporcionada por un usuario puede permitir que los atacantes accedan a servicios internos o endpoints de metadatos de la nube. | Crea una lista de permitidos para los nombres de host, bloquea los rangos de IP privadas y establece tiempos de espera (timeouts). |
dangerouslySetInnerHTML |
Renderizar HTML proporcionado por el usuario sin sanitización abre la puerta a XSS. | Utiliza una librería como DOMPurify o evita el uso de HTML sin procesar por completo. |
Escaneo automático de estos patrones
Las herramientas de análisis estático pueden señalar las construcciones riesgosas enumeradas anteriormente. Una opción ligera es Semgrep, que se ejecuta rápidamente y puede integrarse en los pipelines de CI:
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
El conjunto de reglas incluye comprobaciones para funciones de servidor exportadas, el uso indebido de NEXT_PUBLIC_, redirecciones abiertas, patrones de SSRF e inserción de HTML insegura.
Qué vigilar a continuación
- Actualizaciones del framework – Esté atento a los lanzamientos de Next.js; el equipo podría introducir hooks de autenticación integrados o aislar en un sandbox los endpoints generados.
- Herramientas de la comunidad – Están surgiendo nuevos plugins de ESLint y reglas de Semgrep específicas para Next.js para automatizar las salvaguardas descritas aquí.
- Incidentes en el mundo real – A medida que más proyectos adopten Server Actions, esté atento a los exploits revelados que ilustren el riesgo en la práctica. La detección temprana puede informar las revisiones de seguridad internas antes de que ocurra una brecha.
Conclusión: Una Server Action de Next.js no es una función auxiliar privada; es un endpoint HTTP público en el momento en que la exportas. Trátala como cualquier otra ruta de API —autentica, valida y autoriza—; de lo contrario, la conveniencia de escribir código de servidor junto a la UI puede convertirse rápidamente en un riesgo de seguridad.
