Para entender CSRF me ayudó dejar de pensar en ataques y seguir una petición normal.
Una persona entra en su cuenta, cambia su email y pulsa guardar. Desde la interfaz parece una acción sencilla. Hay un formulario, una cookie de sesión y un endpoint que actualiza el dato.
El problema no está en ese recorrido. Está en que el navegador puede enviar parte de esa misma información aunque la persona esté visitando otra web.
CSRF significa Cross-Site Request Forgery. Consiste en intentar que una aplicación acepte una acción usando la sesión de una persona que ya había iniciado sesión. No hace falta robar la cookie ni conocer su contraseña. Basta con aprovechar que el navegador la adjunta en algunas peticiones.
Es un tema que me interesa porque cambia una idea cómoda sobre los formularios. Que una petición tenga una sesión válida dice quién está autenticado. No dice quién decidió enviar la petición.
La petición que esperabas recibir
En una página de cuenta, un formulario para modificar el email podría ser así
<form action="/cuenta/email" method="post">
<label for="email">Nuevo email</label>
<input id="email" name="email" type="email" />
<button type="submit">Guardar cambios</button>
</form>El servidor recibe la dirección nueva y la cookie de sesión. Con esa cookie localiza a la persona y aplica el cambio.
Hasta aquí parece suficiente. El error es asumir que el servidor solo verá peticiones que hayan pasado por esa pantalla. Un formulario HTML puede enviar datos a otro dominio. Si alguien ya tiene sesión en tu aplicación, su navegador puede acompañar la petición con la cookie según cómo esté configurada.
El servidor no ve el botón, el diseño ni el origen visual del formulario. Solo ve una petición con una sesión que conoce y unos datos que modifican una cuenta.
Ahí es donde deja de bastar con autenticar.
Lo que falta probar
Cuando el servidor recibe una petición para cambiar un dato, necesita resolver dos cosas distintas.
- Quién tiene esa sesión.
- Si la petición pertenece a un flujo que la aplicación permite.
La cookie responde a la primera. Para la segunda hace falta una señal adicional.
Esa señal suele ser un token CSRF generado por el servidor. Se incluye al renderizar el formulario y vuelve con la petición
<form action="/cuenta/email" method="post">
<input type="hidden" name="csrf" value="token-generado-por-el-servidor" />
<label for="email">Nuevo email</label>
<input id="email" name="email" type="email" />
<button type="submit">Guardar cambios</button>
</form>Antes de cambiar el email, el servidor comprueba el token y la sesión. Otro sitio puede intentar mandar un formulario hacia tu dominio, pero no debería poder leer la página de cuenta para conocer un token válido.
El campo oculto por sí solo no tiene nada especial. Un atacante también puede crear campos ocultos. La protección aparece porque el servidor genera, guarda o verifica el valor y rechaza una petición que no lo trae o lo trae mal.
Por eso un token CSRF no se pone en una URL. Una URL puede acabar guardada en el historial, en logs o en una cabecera Referer al seguir un enlace.
SameSite cambia el comportamiento de la cookie
SameSite es una instrucción para el navegador sobre cuándo debe enviar una cookie en peticiones que vienen de otro sitio.
Con Strict, la cookie se restringe más. Puede mejorar la protección, aunque también puede romper recorridos que necesitan llegar desde otro dominio.
Con Lax, algunos casos de navegación siguen funcionando. Para muchas sesiones es un punto de partida útil, pero no convierte en segura cualquier acción que cambia datos.
None permite enviar la cookie en contexto entre sitios y exige Secure. A veces es necesario en integraciones o aplicaciones embebidas. También exige revisar con más cuidado las acciones autenticadas.
No hay una opción que sirva igual para todos los proyectos. Depende de cómo se inicia sesión, si existen subdominios, si la aplicación se integra en otro sitio y qué operaciones permite cada cuenta. El error sería usar SameSite como sustituto de validar las acciones en el servidor.
Cuando no hay formulario visible
En una interfaz moderna, el cambio de email quizá se envíe con fetch y no con un submit tradicional. El problema sigue siendo el mismo si la autenticación depende de cookies.
En ese caso, muchas aplicaciones envían el token CSRF en una cabecera propia. Un formulario HTML creado desde otro origen no puede añadirla libremente. El servidor la valida y CORS decide qué orígenes pueden hacer peticiones a la API.
Aquí es fácil mezclar responsabilidades. CORS no es una protección CSRF completa. Un token no sirve si el servidor no lo valida. Y abrir CORS a cualquier origen porque “la API es pública” puede deshacer parte de lo que creías haber protegido.
Antes de escribir esta lógica desde cero, revisaría la protección que ya trae el framework. Es mejor entender cómo se configura y probarla que inventar un token que nadie volverá a mirar dentro de seis meses.
El frontend sigue teniendo trabajo
El token se genera y se valida en servidor, pero eso no deja al frontend fuera.
Hay que enviarlo con el formulario o con la petición de la API. Hay que evitar que termine en una URL. Hay que entender qué pasa al renovar una sesión, volver atrás en el navegador o mostrar un error cuando el token ya no es válido.
También conviene separar lo que protege cada cosa. HttpOnly evita que JavaScript lea una cookie sensible. Secure evita que viaje por HTTP. SameSite limita ciertos envíos entre sitios. Ninguna de esas opciones demuestra por sí sola que una petición la inició la persona usuaria.
Al revisar errores de seguridad comunes en formularios web me centraba en lo que entra en la aplicación. CSRF añade otra pregunta. ¿Quién ha provocado que esta acción llegue al servidor?
Y XSS sigue siendo un problema relacionado, pero diferente. Si alguien ejecuta JavaScript dentro de tu propio origen, puede leer el token que la página usa. Por eso el token CSRF no compensa un XSS. El artículo sobre XSS explicado para desarrolladores frontend va de esa parte.
Lo que me llevo para proyectos futuros
Ahora mismo no todos mis proyectos tienen cuentas de usuario. Un portfolio estático no necesita este tipo de defensa. Pero cuando construya algo que permita cambiar datos, guardar progreso o ejecutar acciones con una sesión, quiero que la seguridad forme parte del diseño de la petición desde el principio.
No quiero llegar al final con una interfaz terminada y descubrir que el servidor acepta un cambio solo porque llega una cookie válida. La pantalla puede ser clara y el formulario puede validar bien. La decisión importante sigue estando detrás, en qué condiciones acepta el servidor modificar algo.
Eso es lo que me está aportando estudiar seguridad mientras sigo haciendo frontend. Ver que una acción aparentemente pequeña también tiene un recorrido técnico y que el servidor necesita pruebas distintas para problemas distintos.