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
- Copia
src/lib/email/unsubscribe-links.tsy usabuildUnsubscribeLinks()ensrc/app/api/newsletter/subscribe/route.tspara las cabeceras y el enlace del email. - En
src/app/api/newsletter/unsubscribe/route.ts, leeemailytokende la query o del cuerpo (el clic único llega como formulario, no como JSON). Sin alguno de los dos, 400. SiverifyUnsubscribeTokenfalla, 403. - Añade
if (!getSecret()) return false;al principio deverifyUnsubscribeToken(). - Añade
'/api/newsletter/unsubscribe'aORIGIN_VALIDATION_BYPASS_PREFIXESensrc/proxy.ts. - 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. - Para el hook, copia
.claude/hooks/format-lint.tsy el bloquePostToolUsede.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.