CSRF: Vectores de Ataque y Mecanismos de Defensa

Article
Author: mugabe

La Falsificación de Petición en Sitios Cruzados (CSRF) es un tipo de ataque en el que un atacante engaña al navegador de un usuario autenticado para que envíe una solicitud HTTP no deseada a una aplicación web. A diferencia del XSS, el objetivo del CSRF no es el robo de datos, sino realizar acciones en nombre del usuario.

La causa fundamental de esta vulnerabilidad radica en el hecho de que los navegadores adjuntan automáticamente los datos de autenticación (principalmente Cookies) a las solicitudes realizadas a los dominios correspondientes, independientemente de qué recurso haya iniciado la solicitud.

Vectores de Ataque #

Para explotar esta vulnerabilidad, un atacante crea una página que envía discretamente una solicitud al servidor vulnerable. Esta página no necesita ser una versión falsa del sitio vulnerable; podría ser un artículo, una publicación de noticias o cualquier otra cosa. La clave es engañar al usuario para que la visite; el ataque se llevará a cabo de forma silenciosa y puede no levantar ninguna sospecha.

Examinemos las formas en que se puede implementar este ataque.

Ataque mediante Formulario HTML #

La variante más simple y común es un formulario HTML estándar.

Por ejemplo, en nuestro sitio web del Centro de Control de Misiones, hay un formulario de lanzamiento de cohetes disponible solo para el Director de Vuelo:

 1<!-- Formulario en mission-control.internal -->
 2<form
 3  action="/launch"
 4  method="POST"
 5>
 6  <select name="launchpad">
 7    <option value="lc-39a">LC-39A</option>
 8    <option value="lc-39b">LC-39B</option>
 9    <option value="slc-40">SLC-40</option>
10    <option value="slc-41">SLC-41</option>
11  </select>
12  <button type="submit" class="big-red-button">
13    LAUNCH
14  </button>
15</form>

El formulario es procesado por el backend e inicia el lanzamiento de un cohete en la plataforma especificada:

 1func launchHandler(w http.ResponseWriter, r *http.Request) {
 2  // Verificación de sesión 
 3  sessionCookie, _ := r.Cookie("session_id")
 4  session := getSession(sessionCookie)
 5  if session == nil || session.Role != "FlightDirector" {
 6    http.Error(w, "Acceso solo para el Director de Vuelo", http.StatusUnauthorized)
 7    return
 8  }
 9  // Lectura de datos del formulario
10  launchpad := r.FormValue("launchpad")
11  // Ejecución de una acción crítica
12  spaceAPI.InitiateLaunch(launchpad)
13  fmt.Fprintf(w, "¡Cohete en la plataforma %s lanzado!", launchpad)
14}

A primera vista, el método podría parecer seguro: incluye comprobaciones de sesión y rol.

Sin embargo, no contiene ninguna verificación de dónde se originó la solicitud. Un atacante puede crear una página con información interesante y enviar un enlace al desprevenido Director de Vuelo:

 1<html>
 2  <head>
 3    <title>Fotos de la Reunión Anual de Reptilianos</title>
 4  </head>
 5  <body>
 6    <img src="/elon.jpg" />
 7    <img src="/mark.jpg" />
 8    
 9    <!-- formulario oculto en onlyreptiloids.tld -->
10    <form
11      action="https://mission-control.internal/launch"
12      method="POST"
13      id="attack-form"
14    >
15      <input type="hidden" name="launchpad" value="slc-40" />
16    </form>
17    <script>
18      // Envío automático del formulario
19      document.getElementById("attack-form").submit();
20    </script>
21  </body>
22</html>

La víctima solo necesita visitar esta página para que el formulario se envíe automáticamente al sitio objetivo.

Dado que el Content-Type de este formulario es application/x-www-form-urlencoded, el navegador enviará la solicitud junto con la cookie de sesión sin ninguna comprobación preliminar (CORS preflight).

Ataque mediante Solicitud GET #

Vale la pena recordar que las solicitudes GET deben ser, por definición, idempotentes: no deben provocar cambios en el estado del sistema. En el contexto de CSRF, tales solicitudes se consideran seguras por defecto, y los mecanismos de protección para ellas no son obligatorios. Sin embargo, en la práctica, los desarrolladores a menudo descuidan esta regla o permiten el uso del método GET en lugar de POST por error.

Cometimos tal error en el ejemplo anterior con la implementación de launchHandler: no verificamos el método HTTP y usamos r.FormValue, que extrae datos no solo del formulario sino también de los parámetros de consulta (query parameters).

Como resultado, un atacante ni siquiera necesita enviar un formulario; basta con construir un enlace con estos parámetros y, por ejemplo, especificarlo como el src de una imagen:

1<img src="/elon.jpg" />
2<img src="/mark.jpg" />
3<!-- Imagen peligrosa: -->
4<img src="https://mission-control.internal/launch?launchpad=slc-40" />

Para el navegador, no hay diferencia entre solicitar una imagen y llamar a un método de la API. Tan pronto como el Director de Vuelo abra esta página, el navegador intentará cargar la "imagen" junto con las cookies de sesión, y el método inseguro del backend, al no distinguir GET de POST, ejecutará la acción.

Ataque mediante XHR (XMLHttpRequest y fetch) #

Este método se puede aplicar cuando es necesario realizar una solicitud compleja (por ejemplo, a una API JSON-RPC) que no se puede implementar a través de un formulario estándar.

 1// script en el sitio reptiliano
 2fetch("https://mission-control.internal/api/adjust-orbit", {
 3  method: "POST",
 4  headers: {
 5    "Content-Type": "text/plain" // Evadiendo la comprobación preflight del navegador
 6  },
 7  body: JSON.stringify({
 8    "thruster_id": "main-engine",
 9    "action": "burn",
10    "duration_sec": 600
11  }),
12  credentials: "include" // Forzando el envío de cookies con la solicitud
13});

En los navegadores modernos, tales ataques están casi totalmente bloqueados por el mecanismo CORS y la Política de Mismo Origen (Same-Origin Policy). Por lo tanto, para que esto funcione, deben existir errores de configuración específicos en el servidor:

  • Configuración CORS insegura: se permiten solicitudes desde dominios arbitrarios y se habilita la transferencia de credenciales (Access-Control-Allow-Credentials: true).
  • Protección de Cookies debilitada: las cookies de sesión tienen el atributo SameSite=None, o el usuario está utilizando un navegador desactualizado.

Métodos de Defensa #

Para protegerse contra los ataques CSRF, debemos asegurarnos de que la solicitud se haya originado desde una interfaz de confianza.

¡Atención!

Los ejemplos de código se proporcionan solo con fines de demostración. Para mayor legibilidad, pueden carecer de validación, manejo de errores y otras simplificaciones. Por favor, no los use "tal cual" en sus proyectos. Se pueden encontrar ejemplos más detallados y precisos en artículos relacionados dedicados a estos temas.

Configuración de Parámetros de Cookies (SameSite) #

Los navegadores modernos admiten el atributo SameSite para el encabezado Set-Cookie, que restringe la transmisión de cookies durante solicitudes entre dominios:

  • Strict: Las cookies se envían solo dentro del mismo sitio. Esto es efectivo para prevenir la mayoría de los ataques, pero puede ser inconveniente para el usuario; al navegar al sitio a través de un enlace externo (por ejemplo, desde su propio tablero en un dominio diferente), el usuario se encontrará con la sesión cerrada.
  • Lax: Las cookies no se envían con solicitudes POST entre dominios, pero se permiten durante las navegaciones de enlaces estándar (GET). Este valor se utiliza por defecto en Go en la mayoría de los frameworks y librerías modernas.
 1// Configuración de una cookie con los atributos correctos
 2http.SetCookie(w, &http.Cookie{
 3  Name:     "session_id",
 4  Value:    sessionId,
 5  Path:     "/",
 6  HttpOnly: true, // Protección contra XSS
 7  Secure:   true, // Solo a través de HTTPS
 8  // Protección CSRF a nivel de navegador:
 9  SameSite: http.SameSiteLaxMode, 
10})

Vale la pena recordar que estos son atributos relativamente nuevos; los navegadores antiguos pueden ignorarlos, por lo que definitivamente se deben aplicar otros métodos de defensa.

Verificación de Encabezados Origin y Referer #

El servidor puede verificar los encabezados Origin o Referer para asegurarse de que la solicitud provenga de un dominio de confianza.

 1// Ejemplo de middleware para la comprobación de Origin
 2func OriginCheckMiddleware(next http.Handler) http.Handler {
 3  return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 4    // Verificar solo métodos mutables
 5    if r.Method == http.MethodPost || r.Method == http.MethodPut {
 6      source := req.Header.Get("Origin")
 7      // Si Origin está vacío, intentar con Referer
 8      if source == "" {
 9        source = req.Header.Get("Referer")
10      }
11      sourceUrl, _ := url.Parse(source)
12      // Comprobar si Host y Origin/Referer coinciden
13      if sourceUrl.Host != req.Host {
14        http.Error(w, "Intento de lanzamiento cruzado rechazado", http.StatusForbidden)
15        return
16      }
17    }
18    // Continuar procesando la solicitud
19    next.ServeHTTP(w, req)
20  })
21}

Este es un método de defensa decente, pero no está exento de fallos:

  • Aunque los navegadores modernos están obligados a enviar uno de estos encabezados para las solicitudes POST, y su ausencia por sí sola podría ser motivo para rechazar la solicitud, pueden ser eliminados por servidores proxy.
  • En una infraestructura compleja con múltiples dominios, mantener una lista de dominios de confianza puede ser laborioso.

Verificación de Encabezados Sec-Fetch-* #

Además de Origin y Referer, vale la pena analizar los encabezados Sec-Fetch-*, que son añadidos por los navegadores modernos y no pueden ser falsificados mediante scripts dentro del navegador.

Específicamente, el encabezado Sec-Fetch-Site puede tomar valores como same-origin, same-site, cross-site y none.

  • same-origin: la solicitud se realizó desde el mismo dominio, protocolo y puerto; el nivel más seguro.
  • same-site: la solicitud se realizó desde un subdominio del mismo sitio (por ejemplo, de api.mission-control.internal a mission-control.internal).
  • cross-site: una solicitud desde cualquier otro sitio.
  • none: la solicitud fue iniciada por el usuario (entrada en la barra de direcciones, marcadores, etc.).
 1// Ejemplo de middleware para la comprobación de Sec-Fetch-Site
 2func FetchMetadataMiddleware(next http.Handler) http.Handler {
 3  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 4    // Verificar solo métodos mutables
 5    if r.Method == http.MethodPost || r.Method == http.MethodPut {
 6      site := r.Header.Get("Sec-Fetch-Site")
 7
 8      // Permitir solo solicitudes de nuestro dominio o entradas directas
 9      if site != "same-origin" && site != "same-site" && site != "none" {
10        http.Error(w, "Solicitud bloqueada por la política Fetch Metadata", http.StatusForbidden)
11        return
12      }
13    }
14    
15    next.ServeHTTP(w, r)
16  })
17}

Además, puede ser útil analizar encabezados como Sec-Fetch-Mode (cómo se hizo la solicitud: enlace, formulario, script), Sec-Fetch-Dest (qué se está solicitando: documento, imagen, etc.) y Sec-Fetch-User (participación del usuario: clic o automático).

Esta es una excelente manera de configurar de manera flexible los métodos de solicitud permitidos, pero no olvide que, al igual que SameSite, estos encabezados podrían no ser enviados por navegadores más antiguos.

Tokens Criptográficos (Desafío-Respuesta) #

Si bien todos los métodos anteriores aumentan la seguridad, no están exentos de inconvenientes y solo deben usarse como capas de defensa suplementarias.

El método más confiable y universal es agregar un valor secreto único (token CSRF) a cada solicitud.

Dependiendo de si su sistema tiene estado (con una sesión) o no tiene estado (sin sesiones), se pueden aplicar dos enfoques diferentes:

  • Synchronizer Token Pattern: El servidor genera un token, lo guarda en la sesión del usuario y lo inserta en el formulario HTML. Al recibir una solicitud, el servidor compara el token del formulario con el token en la sesión.
  • Double Submit Cookie: El token se envía tanto en una cookie como en el cuerpo de la solicitud. El servidor verifica que coincidan sin mantener el estado de su lado (stateless).

En los siguientes artículos, analizaremos en detalle cada enfoque y sus detalles de implementación.