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

Seguridad: la baja de la newsletter exige un token firmado

Cualquiera podía dar de baja de tu newsletter a otra persona sabiendo solo su email. Además, el botón de baja en un clic de Gmail vuelve a funcionar y el hook de formato de Claude deja de reformatear el repo entero.

Segunda versión de seguridad del día. Cierra un fallo menor que la 2.1.2, pero que conviene aplicar: nadie podía leer datos, aunque cualquiera podía borrar suscriptores de tu newsletter.

La baja de la newsletter pide token

POST /api/newsletter/unsubscribe daba de baja cualquier email que le llegara, sin comprobar nada. Bastaba con saber el email de alguien para sacarlo de tu lista.

Ahora exige un token firmado con BETTER_AUTH_SECRET, el mismo que ya llevaban los enlaces de la página de baja. Sin token responde 400, con un token inválido o de otro email responde 403, y con uno válido siempre da la misma respuesta, para no revelar quién está suscrito. Si falta el secreto, se rechaza cualquier baja en lugar de firmar con una clave vacía.

El botón de baja de Gmail funciona

Las cabeceras List-Unsubscribe apuntaban al endpoint sin email ni token, y además la protección de Origin rechazaba el clic único de los proveedores de correo, que no envían esa cabecera. El botón «Cancelar suscripción» de Gmail nunca llegó a funcionar. Ahora las cabeceras llevan el email y el token, y el endpoint queda fuera de la validación de Origin: el token es la prueba.

El hook de formato de Claude

El hook que formatea cada fichero que edita Claude leía una variable que Claude Code no define. Con la ruta vacía, Prettier reformateaba el repo entero en cada edición, y los errores de ESLint se perdían. Ahora formatea solo el fichero editado y le devuelve a Claude los errores que no se corrigen solos, para que los arregle en el mismo turno.

Si ya tienes un proyecto

  1. Copia src/lib/email/unsubscribe-links.ts y usa buildUnsubscribeLinks() en src/app/api/newsletter/subscribe/route.ts para las cabeceras y el enlace del email.
  2. En src/app/api/newsletter/unsubscribe/route.ts, lee email y token de la query o del cuerpo (el clic único llega como formulario, no como JSON). Sin alguno de los dos, 400. Si verifyUnsubscribeToken falla, 403.
  3. Añade if (!getSecret()) return false; al principio de verifyUnsubscribeToken().
  4. Añade '/api/newsletter/unsubscribe' a ORIGIN_VALIDATION_BYPASS_PREFIXES en src/proxy.ts.
  5. Revisa tus propios emails con grep -rn "unsubscribe" src/lib/email src/app/api: un enlace de baja sin token deja de funcionar, y un suscriptor que no puede darse de baja es un problema legal.
  6. Para el hook, copia .claude/hooks/format-lint.ts y el bloque PostToolUse de .claude/settings.json.

No hay migraciones ni variables nuevas. Ojo: si rotas BETTER_AUTH_SECRET, los enlaces de baja que ya están en los buzones dejan de valer.