21 de Octubre, 2011

Ocho años, y un captcha

¡Casi se me pasa! Hoy es el aniversario de esta bitácora, y me he dado cuenta porque el año pasado no se me pasó ;).

Siempre comento lo mismo, y los números siguen empeorando: 1426 anotaciones (solo 91 historias este año), 2794 comentarios y unas 700 visitas diarias; que mucho me parece para haber escrito tan poco.

Además este año he visto como el spam automatizado me ha cogido con ganas, hasta el punto que esta semana he tenido que implementar un captcha (de esos que yo fallo constantemente; ¿seré humano?).

La verdad es que con el viejo CMS que movía la bitácora, antes de pasar a Python y flamantes frameworks, no llegué a tener problemas de spam; aunque fuera a costa de los lectores con el malévolo form_key (¿a quién no se le comió alguna vez un comentario?).

Pero precisamente esta semana una red de spam me ha puesto en su punto de mira, y tras añadir un par de bloques enteros de máquinas supuestamente bajo control de la red, me he cansado de borrar comentarios y añadir reglas a iptables.

Mi propuesta de captcha es realmente sencilla y no consume nada de recursos (no necesita sesiones ni almacenamiento temporal, que creo que es lo que se necesita para una solución completa).

Para empezar utilizo la protección de Tornado para ataques CSRF, que se encarga de implementar con una cookie y un campo obligatorio que tengo que añadir a cada formulario enviado con POST; esto me da cierta aleatoriedad.

En segundo lugar tengo una clave privada que utilizo para generar un hash de la respuesta, de forma que:

respuesta_hash = MD5(f(secreto, respuesta, csrf))

La fórmula f es también secreta, aunque tampoco tiene mucha importancia, porque el captcha se puede reventar si sabemos el hash correcto para una respuesta, y el cookie de la protección CSRF que le corresponde.

Es decir, que si la red de spam tiene interés, puede saltarse el captcha, pero implicaría un ataque a medida... y probablemente no va a pasar (y si pasa, cambio el invento, y que vuelvan a buscar el hash, etc; y así ad infinitum).

Al final es una implementación pura y dura en 10 minutos (no exagero), con dos partes. Primero el código que genera el formulario de los comentarios:

import random
from hashlib import md5

# valores de prueba
secreto = 'secreto-secreto'
xsrf_token = 'esto-es-un-hash-que-va-en-el-cookie'

# captcha
numeros = [ 'cero', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis',
            'siete', 'ocho', 'nueve', ]
a = random.choice(range(9))
b = random.choice(range(9))
pregunta = '¿Cuántos son %s + %s? (númerico)' % (numeros[a], numeros[b])
respuesta_hash = md5(f(secreto, xsrf_token, str(a+b))).hexdigest()

print pregunta, "; con hash:", respuesta_hash

Una función f puede ser tan sencilla como:

def f(secreto, xsrf, respuesta):
    return "%s%s%s" % (secreto, xsrf, respuesta)

Y luego, cuando nos llega el formulario con el comentario, la respuesta y su hash, la verificamos:

respuesta_hash = md5(f(secreto, xsrf_token, str(form['respuesta']))).hexdigest()

if respuesta_hash != form['respuesta_hash']:
    raise tornado.web.HTTPError(403)

Creo que se entiende bien (de hecho, con la explicación, el código casi sobraba :P).

Como consecuencia de ser una implementación rápida, si la entrada de la respuesta falla, confío en que el navegador conserve los datos del formulario al volver atrás, porque no me he calentado mucho la cabeza en esa parte :P.

En definitiva, aquí seguimos tras 8 años, y resistiendo con más o menos gracia incluso al spam más molesto.

Actualización: supongamos que tienes un almacenamiento como Redis ya funcionando, y además te sobran 5 minutos :).

En realidad no necesitas ni el componente secreto, ni la cookie anti-csrf, ni f; simplemente genera una clave aleatoria para almacenar en redis la solución (marcando la clave para expirar en un par de horas), y ahí tienes el captcha perfecto.

Aún se puede analizar fácilmente la pregunta y hacer el cálculo, pero eso ya creo que implicaría un interés especial en colar comentarios con spam en esta humilde bitácora (pista: no :D).

Anotación por Juan J. Martínez, clasificada en: blog, python.

Hay 3 comentarios

Gravatar

Feliz blogocumpleaños! Lo de los comentarios, mucha paciencia. ;)

por corsaria, en 2011-10-21 23:15:09

Gravatar

Gracias!

Hasta hace poco el spam me venía bien para saber que los comentarios todavía funcionan; ahora me voy a quedar con la duda :D

por Juanjo, en 2011-10-22 09:25:03

Gravatar

Muchas felicidades, por los ocho años y por toda la información que habitualmente podemos leer en blackshell. Espero seguirte leyendo dentro de otros 8 años más y haber brindado físicamente en alguna ocasión por ello ;).

por r0sk, en 2011-10-25 12:03:25

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: