Saltar al contenido principal
Volver al Changelog
v2.1.4 · Parche

Seguridad: la página de baja pide confirmación

Los escáneres de enlaces del correo corporativo podían dar de baja a un suscriptor sin que él hiciera clic. Ahora la página pregunta antes, y el endpoint de baja limita los intentos fallidos.

Tercera versión de seguridad del día, y cierra los dos cabos que dejó la 2.1.3 en la baja de la newsletter.

La página de baja ya no da de baja al abrirla

La página /[locale]/unsubscribe daba de baja en cuanto se abría con un enlace válido. El problema es que no siempre la abre una persona: Microsoft Safe Links, Proofpoint y otros escáneres del correo corporativo visitan cada enlace de un email antes de entregarlo, y los navegadores a veces precargan enlaces. Un suscriptor podía acabar fuera de tu lista sin haber hecho clic.

Ahora, con un enlace válido, la página pregunta «¿Darte de baja de la newsletter de X?» y muestra el botón Confirmar baja. La baja solo se hace al pulsarlo. Si el enlace no es válido o le falta el token, la página sigue mostrando «Enlace no válido» como hasta ahora. Los textos están en español y en inglés.

El botón «Cancelar suscripción» de Gmail y del resto de clientes (el clic único de RFC 8058) no cambia: sigue dando de baja directamente, porque es una petición deliberada del proveedor de correo y los escáneres nunca la hacen.

Límite de intentos fallidos en el endpoint de baja

POST /api/newsletter/unsubscribe solo tenía el límite general de la API. Ahora admite 10 intentos fallidos por hora y por IP (sin token o con un token inválido) y responde 429 a partir del undécimo.

Una baja con token válido no cuenta nunca, venga de la página, de una llamada propia o del botón de Gmail, y pasa aunque esa IP esté bloqueada por fallos. El token ya demuestra que la baja es legítima, y los proveedores de correo mandan el clic único desde unas pocas IPs compartidas: un límite por IP sobre esas peticiones acabaría negando bajas reales.

Si ya tienes un proyecto

  1. Copia src/features/marketing/components/unsubscribe-content.tsx, src/features/marketing/components/unsubscribe-confirm.tsx y src/features/marketing/hooks/use-unsubscribe-confirm.ts, y añade el bloque unsubscribe de src/config/locales/{es,en}/marketing.json a tus ficheros de idioma.
  2. Si personalizaste la página de baja, conserva tu diseño pero saca de ella la llamada a unsubscribeContact(): la página solo debe comprobar el token y mostrar el botón, que hace un POST del email y el token a /api/newsletter/unsubscribe.
  3. En src/lib/security/rate-limit/rate-limiter.ts, añade a RATE_LIMITS una entrada NEWSLETTER_UNSUBSCRIBE con maxRequests: 10 y windowSeconds: 60 * 60.
  4. En src/app/api/newsletter/unsubscribe/route.ts, aplica el límite solo en las respuestas 400 y 403: antes de devolverlas, llama a rateLimitMiddleware(request, RATE_LIMITS.NEWSLETTER_UNSUBSCRIBE, { prefix: 'newsletter-unsubscribe' }) y, si no es null, devuelve su 429. El camino con token válido no se toca.
  5. Reconstruye desde limpio (rm -rf .next && bun run build) antes de desplegar.

No hay migraciones ni variables nuevas, y los enlaces de baja que ya están en los buzones siguen funcionando: el token y las direcciones son los mismos. Si tienes más de una instancia, el límite es por instancia salvo que configures REDIS_URL.