Ir al contenido

FundamentosLección 08

Mismo origen, CSRF y CORS

La regla que protege a los navegadores, el ataque que abusa de las cookies automáticas y el mecanismo que relaja la regla de forma controlada. Tres piezas que solo se entienden juntas.

Intermedio16 min de lectura

Al terminar sabrás

  • Saber qué es un origen y qué permite y prohíbe la política de mismo origen.
  • Explicar un ataque CSRF y por qué funcionan el token CSRF y SameSite.
  • Entender qué hace CORS, qué es un preflight y por qué CORS no protege tu API.
  • Decidir cuándo una API puede prescindir de la protección CSRF.

Tu navegador tiene abiertas a la vez la web de tu banco, tu correo y una página cualquiera. Todas ejecutan JavaScript en el mismo programa. Lo que impide que la página cualquiera lea tu correo es una regla del navegador: la política de mismo origen. CSRF y CORS son, respectivamente, un ataque que la esquiva y un mecanismo para relajarla.

https://esquemaapp.example.comhost:443puerto/api/notes?id=5ruta (no forma parte del origen)

origen

Un origen es la tupla esquema + host + puerto. La ruta no cuenta.
URL comparada con https://app.example.com¿Mismo origen?Por qué
https://app.example.com/otra/rutaSíSolo cambia la ruta
http://app.example.comNoCambia el esquema
https://api.example.comNoCambia el host (un subdominio es otro origen)
https://app.example.com:8443NoCambia el puerto

Mismo sitio (same-site) es un concepto más amplio: app.example.com y api.example.com son orígenes distintos pero el mismo sitio (example.com). SameSite en las cookies se basa en el sitio, no en el origen.

Una página de evil.example puede:

  • Enviar un formulario POST a bank.example (y el navegador adjuntará las cookies de bank.example según su SameSite).
  • Cargar imágenes, scripts y hojas de estilo de cualquier origen.
  • Hacer fetch a cualquier origen… pero no leer la respuesta salvo que ese origen lo permita con CORS.

Cross-Site Request Forgery hace que el navegador de la víctima envíe una petición que la víctima no quería, aprovechando que el navegador adjunta las cookies automáticamente.

Navegador de la víctimagatos-graciosos.examplebank.example1Inicia sesiónrecibe cookie SESSION de bank.example2Visita una página de gatos3HTML con un formulario oculto que se autoenvía4POST /transfer to=666&amount=1000Cookie: SESSION=… (adjuntada sola)5200 OK: transferencia hecha
El atacante no ve la respuesta, pero no le hace falta: la transferencia ya ocurrió.
La página del atacante
<form action="https://bank.example/transfer" method="POST" id="f">
<input type="hidden" name="to" value="666">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.getElementById('f').submit();</script>
  1. Token CSRF (sincronizador): el servidor genera un valor secreto aleatorio ligado a la sesión, lo incluye en sus formularios o lo entrega al JavaScript de su propio origen, y exige que cada petición que cambia estado lo devuelva. La página atacante no puede leerlo (política de mismo origen), así que no puede incluirlo.
  2. Cookies SameSite=Lax o Strict: el navegador no adjunta la cookie a peticiones POST iniciadas desde otro sitio. Es una defensa excelente, pero no cubre a navegadores antiguos ni ataques desde otro subdominio del mismo sitio. Úsala además del token, no en su lugar.
  3. No cambiar estado con GET: GET, HEAD y OPTIONS deben ser seguros (sin efectos). SameSite=Lax deja pasar los GET de navegación, y casi ningún framework protege GET con token.
  4. Comprobar Origin/Sec-Fetch-Site como capa extra: el navegador indica de dónde viene la petición.

Tu front en https://app.example.com necesita leer respuestas de tu API en https://api.example.com. Son orígenes distintos, así que el navegador bloquearía la lectura. CORS (Cross-Origin Resource Sharing) es la forma en que la API le dice al navegador: “este origen sí puede leerme”.

Para peticiones “no simples” (con Authorization, con Content-Type: application/json, o con métodos como PUT y DELETE), el navegador pregunta primero con un preflight:

Frontapp.example.comNavegadorAPIapi.example.com1fetch(…/notes, { method: 'DELETE', headers: {Authorization} })2OPTIONS /notes/42Origin + Access-Control-Request-Method: DELETE3204 + Access-Control-Allow-Origin: https://app.example.comAllow-Methods: DELETE · Allow-Headers: Authorization4DELETE /notes/42 + Authorization5204 No Content6la respuesta es legible
El preflight es una petición OPTIONS sin credenciales. Si la API no la responde bien, la petición real ni se envía.
OPTIONS /notes/42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Allow-Credentials: true
CasoCSRF
API stateless autenticada con Authorization: BearerNo aplica: se puede desactivar
API usada solo por otros servicios (nunca un navegador)No aplica
Aplicación web con sesión y formulariosActivado
SPA que usa una cookie de sesión (patrón BFF)Activado (token en cabecera)
JWT guardado en una cookieActivado: la cookie vuelve a ir sola
Webhook externo dentro de una app con sesiónExceptuar solo esa ruta y validar la firma del webhook

Mini lab · 12 min

Ataca y defiende

Objetivo: Razonar sobre CSRF y CORS en escenarios concretos antes de configurarlos en Spring.

Tu banco usa sesión con cookie SESSION (HttpOnly; Secure, sin atributo SameSite) y tiene POST /transfer. Su front vive en https://bank.example y llama a la API https://api.bank.example con fetch(..., { credentials: 'include' }).

  1. ¿Funcionaría el formulario oculto del atacante de arriba? ¿Qué dos cambios lo bloquearían?
  2. La API responde a todo preflight con Access-Control-Allow-Origin: <el Origin recibido> y Allow-Credentials: true. ¿Qué puede hacer ahora evil.example que antes no podía?
  3. Un compañero dice: “desactivemos CSRF, total ya tenemos CORS configurado”. ¿Qué le respondes?
Pista

Recuerda la asimetría: la política de mismo origen bloquea leer, no enviar.

Mostrar respuestaOcultar respuesta
  1. Sí funcionaría: un formulario HTML es una petición “simple” (no hay preflight) y, sin SameSite, el navegador adjunta la cookie. Lo bloquean: (a) exigir un token CSRF en POST /transfer, y (b) marcar la cookie con SameSite=Lax (o Strict). Ambas juntas son la defensa recomendada.
  2. Ahora evil.example puede hacer fetch con credentials: 'include' y leer la respuesta: el saldo, los movimientos, y también el token CSRF si hay un endpoint que lo devuelve, con lo que podría saltarse la protección CSRF. Hay que limitar Allow-Origin a https://bank.example.
  3. Que CORS no protege contra CSRF: un formulario POST simple no lanza preflight, el navegador lo envía igualmente con la cookie, y el servidor ejecuta la acción aunque el atacante no pueda leer la respuesta. Mientras la autenticación viaje en una cookie, CSRF debe seguir activo.