joe di castrohttp://joedicastro.comThu, 14 Oct 2010 01:42:00 +0200Combatir el spam en Drupalhttp://joedicastro.com/combatir-el-spam-en-drupal.html<h3 id="articulo_publicado_originalmente_en_el_antiguo_sitio_deaparatoscom">Articulo publicado originalmente en el antiguo sitio deaparatos.com</h3> <p style="text-align: center;"><img src="pictures/spam_stats.png" alt="deaparatos spam statistiscs" title="estadísticas de spam en deaparatos" height="550" width="593" /></p> <p>En esta gráfica se puede observar la <strong>disminución a lo largo del tiempo de los ataques de spam a este sitio</strong>, <strong>deaparatos.com</strong>, que funciona sobre <strong>Drupal</strong>. Esto se ha conseguido gracias a una doble estrategia:</p> <ul> <li>emplear uno de los mejores módulos antispam existentes para Drupal, <strong>Mollom</strong></li> <li>emplear un script en python de elaboración propia, <strong>ban_drupal_spammers.py</strong></li> </ul> <p>Esta doble estrategia no solo ha conseguido una más que <strong>notable reducción</strong> de la incidencia <strong>del molesto spam</strong> en este sitio, <strong>de casi un 70%</strong>, si no que además ha conseguido <strong>una más que notable reducción del ancho de banda consumido por los spammers</strong>, como se puede observar en la siguiente tabla:</p> <p>Estadísticas de Trafico generado por ataques de spam en deaparatos.com</p> <table> <thead> <tr> <th>Estrategia</th> <th>Días</th> <th>Ataques</th> <th>Trafico (GB)</th> <th>Media pagina (KB)</th> <th>Trafico mes (MB)</th> </tr> </thead> <tbody> <tr> <td>Mollom</td> <td><strong>359</strong></td> <td>48741</td> <td>7,116</td> <td>146,000</td> <td>602,927</td> </tr> <tr> <td>Mollom + script</td> <td><strong>359</strong></td> <td>358666</td> <td>0,016</td> <td>0,046</td> <td>1,389</td> </tr> <tr> <td><strong>Total</strong></td> <td><strong>359</strong></td> <td><strong>407407</strong></td> <td><strong>7,133</strong></td> <td><strong>17,507</strong></td> <td><strong>604,316</strong></td> </tr> </tbody> </table> <p>Si solo hubiera empleado el modulo Mollom, sin emplear mi script</p> <table> <thead> <tr> <th>Trafico spam</th> <th>Calculo</th> <th>Trafico (GB)</th> <th>Ahorro (GB)</th> </tr> </thead> <tbody> <tr> <td><strong>Trafico total</strong></td> <td>(358666 * (146,000 – 0,046) KB) + 7,133 GB =</td> <td>59,481</td> <td><strong>52,349</strong></td> </tr> <tr> <td><strong>Trafico mensual</strong></td> <td>((59,465 GB * 365) / 359) / 12) MB =</td> <td>5,040</td> <td><strong>4,435</strong></td> </tr> </tbody> </table> <p>Como podemos ver en las cifras mostradas de esta tabla, <strong>se ha conseguido en</strong> un periodo de aproximadamente <strong>un año reducir el ancho de banda consumido por los ataques de spam en más de 52 Gigabytes!</strong>, una autentica barbaridad de tráfico que de otro modo se hubiera malgastado. Estamos hablando de <strong>un ahorro de</strong> consumo de tráfico <strong>de casi 4,5 Gigabytes al mes!!!</strong> Un ahorro de ancho de banda mensual que en un hosting compartido puede tranquilamente suponer el cambio de un plan de hosting a otro, simplemente basta con que los molestos spammers pongan tu sitio web en su punto de mira. Y <strong>ha de tenerse en cuenta</strong> una cosa, <strong>que este trafico mensual hubiera sido muy superior si</strong> esta doble estrategia <strong>no hubiera conseguido reducir el numero de spammers en un 69.25%</strong>, no quiero ni pensar en las cifras que hubieran resultado...</p> <p>Para que nos hagamos una idea del ahorro de ancho de banda que ha supuesto el emplear mi script <strong>python</strong>, en el siguiente gráfico podemos ver la diferencia entre emplear solo <strong>Mollom</strong> y emplear <strong>Mollom</strong> combinado con <strong>ban_drupal_spammers.py</strong></p> <p style="text-align: center;"><img src="pictures/Ahorros.png" alt="Eficacia del script" title="Eficacia del script" height="292" width="600" /></p> <p>El gráfico es meridianamente claro, como podemos ver, <strong>por cada 1% de ataques que son rechazados por ban_drupal_spammers.py</strong> y no por Mollom, <strong>ahorramos un 1% de ancho de banda</strong>, tanto en el peso por página como en el tráfico total. Como podemos ver, <strong>hemos ahorrado un total de un 88% de ancho de banda del trafico que sería generado por los ataques de spam en deaparatos.com</strong></p> <p>Después de comprobar la eficacia de esta doble estrategia durante más de un año (las estadísticas se interrumpen antes por el cambio de hosting) voy a explicaros el porqué y el como he llegado a ella, a continuación. También se puede ver el script que ha marcado la diferencia de tráfico.</p> <h2 id="el_spam_en_internet">El spam en internet</h2> <p>El <a href="http://es.wikipedia.org/wiki/Spam">spam</a> es una de las lacras más tediosas y difíciles de combatir en Internet, por no mencionar las tareas delictivas que se apoyan en él. Después de 15 años combatiendo el spam en el correo electrónico, el problema aún está lejos de solucionarse, si bien es cierto que con una adecuada configuración de las herramientas de correo, se ha convertido en una molestia trivial para el usuario final. Pero para los servidores de correo y el tráfico de internet sigue siendo un problema de dimensiones colosales, la lucha contra el mismo se ha convertido una tarea titánica en la que se invierten ingentes sumas de dinero todos los años. De hecho gran parte del tráfico de toda internet se debe al spam (hay quien arroja cifras del 80%, e incluso superiores al 90%), lo que ha acarreado un costosísimo sobredimensionamiento en el equipamiento de proveedores de internet y servidores.</p> <p>Como antes comentaba, lejos de una solución definitiva (en gran medida depende de un tipo muy común de usuario final con escasa cultura informática), esta lacra se expandió hace unos años a hilos en foros, a comentarios en blogs, redes sociales, irc,... es decir, se ha expandido por toda la red. La explosión de la llamada web 2.0 no ha incrementado si no este problema, multiplicándolo. Y he aquí como un problema que afectaba a los usuarios de email y a los proveedores de internet, se ha convertido también en un gran problema para los webmasters. Todo aquel que gestione un sitio web, ha tenido que enfrentarse antes o después con este maldito problema. Un problema que no solo se traduce en cientos o miles de detestables mensajes de spam, que se han de combatir de uno u de otro modo (algunos o bien se rinden o bien tienen abandonados sus sitios y se convierten en auténticos cementerios de spam), si no que además se traduce en un serio problema para el trafico de una web. El número de solicitudes que producen los ataques de spam puede llegar a ser tan elevado, que congestione totalmente ya no solo la página, si no el servidor cuando se trate de un hosting compartido, convirtiéndose casi de facto en un <a href="http://es.wikipedia.org/wiki/DoS">ataque DoS</a> en toda regla. Aún sin llegar a este indeseable extremo, el incremento del tráfico en el sitio debido al spam puede llegar a suponer un porcentaje muy importante del ancho de banda contratado (incluso más del 50% con contramedidas ineficientes), con los consiguientes perjuicios económicos que suponen al webmaster. Los spammers siempre han ido por delante de las contra-medidas, y la actual situación, con extensas <a href="http://es.wikipedia.org/wiki/Botnet">botnets</a> a su disposición y con el <a href="http://es.wikipedia.org/wiki/Cloud_computing">cloud computing</a> (se ha detectado el año pasado la primera <a href="http://www.idg.es/pcworldtech/Los-hackers-controlan-una-botnet-desde-Amazon-EC2/doc88089-actualidad.htm">botnet que empleaba los servicios de Amazon EC2</a>) , nos ha llevado a un combate continuo en las que tienen todas las de ganar a medio plazo... y observo esto con cierta tristeza, por que entiendo que la solución final pasa necesariamente por la educación del usuario final, haciéndole inmune a los -en gran medida patéticos, infantiles, ridículos y chapuceros- reclamos del spam. Y esto último desgraciadamente dista mucho de acercarse a una situación ideal. También cabe mencionar que <a href="http://googlewebmaster-es.blogspot.com/2009/12/comentarios-spam-la-dura-realidad.html?utm_source=feedburner&amp;utm_medium=feed&amp;utm_campaign=Feed%3A+ElBlogParaWebmasters+%28El+Blog+para+Webmasters%29">el spam también perjudica al posicionamiento de una web,</a> a su prestigio, a su funcionalidad, a su aspecto, etc.</p> <h2 id="deaparatoscom_y_el_spam">deaparatos.com y el spam</h2> <p>Y <strong>deaparatos</strong> no está exento de esta amenaza, de hecho se había convertido en un serio problema en el 2009. Este sitio está gestionado con <a href="http://drupal.org/">Drupal</a>, y después de probar con distintos módulos y métodos, unos más frustrantes que otros, ninguno solucionaba por completo el problema, ni me satisfacía como solución. Al final, combinando el módulo más idóneo para combatir esta plaga (idóneo por resultados y por comportamiento) con un script de factura propia en <a href="http://drupal.org/project/Modules">Python</a>, he logrado, no acabar con todo el spam (se me antoja tarea cuasi imposible), pero si minimizar sus efectos a un nivel muchísimo más que aceptable. Y minimizar los efectos tanto a la hora de impedir/eliminar los comentarios spam, como de reducir el abultado tráfico que estos ataques consumían. ¿Por qué un script en <strong>Python</strong>? bueno buscaba algo rápido, un prototipo para probar la solución que tenia en la cabeza y porque estoy "enamorado" de este lenguaje de programación. Quizás si veo que merece la pena, me plantee migrarlo a PHP y convertirlo en un módulo de <strong>Drupal</strong>, o bien modifique el modulo oficial que estoy empleando para mitigar el spam y le incorpore el código que empleo ahora. Bueno, veamos como he llevado a cabo esta solución y porqué.</p> <p>Generalmente los métodos para combatir el spam se centran en:</p> <ul> <li>medidas activas: análisis <a href="http://es.wikipedia.org/wiki/Heuristica">heurísticos</a>, <a href="http://es.wikipedia.org/wiki/Inferencia_estad%C3%ADstica">filtros estadísticos</a> (<a href="http://en.wikipedia.org/wiki/Bayesian_spam_filtering">bayesianos</a>), diferenciación de <a href="http://es.wikipedia.org/wiki/Bot">bots</a>/humanos (<a href="http://es.wikipedia.org/wiki/Captcha">captchas</a>), filtros por <a href="http://es.wikipedia.org/wiki/Host">host</a>/email, mediante <a href="http://es.wikipedia.org/wiki/Cookie">cookies</a>, <a href="http://es.wikipedia.org/wiki/Timestamp">timestamps</a>, filtros por <a href="http://es.wikipedia.org/wiki/User_agent">user-agent</a>, ...</li> <li>medidas pasivas: <a href="http://es.wikipedia.org/wiki/Ofuscaci%C3%B3n">ofuscar</a> direcciones de correo, moderación de comentarios, <a href="http://es.wikipedia.org/wiki/Nofollow">enlaces nofollow</a>, permisos de publicación, políticas de contraseñas, campos ocultos mediante <a href="http://es.wikipedia.org/wiki/Css">css</a>/<a href="http://es.wikipedia.org/wiki/Javascript">javascript</a>, cerrar los comentarios de un post pasado un tiempo, ...</li> </ul> <p>De entrada tenía, y tengo, algo muy claro, no voy a emplear ninguno de estos tres métodos habituales: <em>captchas</em>, <em>moderación de comentarios</em> y <em>requerir registro</em> para enviar comentarios. Y es una decisión inamovible, no pienso claudicar de ningún modo en este sentido. Son métodos que o bien me harían perder un tiempo del que ni dispongo, ni estoy dispuesto a perder, o bien suponen un incordio que me personalmente me incomodan mucho cuando me los encuentro en otros sitios y por los que no quiero hacer pasar a mis lectores. Esto evidentemente deja fuera algunas de las medidas más efectivas para combatir el spam, pero son medidas en las que el usuario o el webmaster siempre pierden, de un modo u otro, y no estoy dispuesto a permitir que los spammers condicionen en ningún modo el compartimiento de este sitio. Aunque suene contradictorio con lo que acabo de decir, el método que voy a comentar aquí, y que empleo actualmente, emplea en alguna medida el uso de captchas, aunque de modo tan limitado, que afecta a menos del 0,5% de los comentarios enviados. Digamos que lo acepto como una razonable excepción a la regla. Si empleo en cierta medida algunos de los otros métodos.</p> <p style="text-align: center;"><img src="pictures/ammap.png" alt="Eficacia del script" title="Eficacia del script" height="419" width="629" /></p> <p>En este mapa podemos ver el país de origen de los ataques de spam contra deaparatos.com</p> <h2 id="drupal_y_el_spam">Drupal y el spam</h2> <p>Con que armas contamos en <strong>Drupal</strong> para combatir el spam? Por un lado tenemos el clásico modulo <a href="http://drupal.org/project/spam">Spam</a>, que emplee en este mismo sitio durante más de dos años, y que su mayor ventaja es contar con un <a href="http://es.wikipedia.org/wiki/Clasificador_bayesiano_ingenuo">filtro Bayesiano</a>. Este módulo es usado actualmente en unos 4.893 sitios <sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup> con <strong>Drupal</strong>. Durante mucho tiempo funcionó perfectamente, de vez en cuando se colaba algún comentario spam, pero era cuestión de reportarselo al modulo y el iba aprendiendo, así como podíamos crear nuestros propios filtros personalizados. El problema comenzó cuando los que se colaban ya eran unos 20 spam diarios y aumentando, y entonces ya no era viable, ni cómodo, perder tanto tiempo para que el filtro bayesiano aprendiera a combatir unos ataques que eran cada vez más sofisticados. Así que tocaba mirar otra alternativa de entre alguna de las disponibles:</p> <ul> <li><a href="http://drupal.org/project/captcha">Captcha</a>, uno de los más usados en <strong>Drupal</strong> (<strong><em>80.286 sitios</em></strong>), y uno de los métodos más empleados en la red. Es la clásica opción donde mediante una pregunta al usuario se intenta diferenciar entre humano y maquina. Los captchas pueden ser de varios tipos, desde cálculos matemáticos sencillos hasta gráficos donde se encuentran unos caracteres ofuscados y que el usuario debe introducir. Hay varios módulos que lo complementan, aportando distintos tipos de captchas, donde <a href="http://drupal.org/project/recaptcha">reCaptcha</a> es uno de los más empleados (16.684). </li> <li><a href="http://drupal.org/project/akismet">Akismet</a>, todo un clásico, basado en el servicio homónimo, <a href="http://wordpress.org/">Akismet</a>, que creara en su día <a href="http://wordpress.org/">Wordpress</a> y que hoy es sostenido por <a href="http://automattic.com/">Automattic</a>, empresa donde trabajan la mayoría de los desarrolladores oficiales de <strong>Wordpress</strong>. Es uno de los métodos más difundidos en la red, en parte por venir de la mano de la empresa más emblemática de los blogs. Esta implementación del servicio <strong>akismet</strong> es ya un tanto antigua (ya no tiene soporte) y hay un modulo más reciente que lo supera y aporta más funcionalidades, <strong>Antispam</strong>, también en esta lista. Es usado actualmente por solo <strong><em>947 sitios</em></strong>.</li> <li><a href="http://drupal.org/project/spamicide">Spamicide</a>, se basa en la premisa de que la mayoría de los ataques spam se producen con bots que acceden a la página con navegadores en modo texto (<a href="http://es.wikipedia.org/wiki/Script_%28inform%C3%A1tica%29">scripts</a> en realidad), por lo tanto ni hacen uso de <strong>css</strong>, ni de <strong>javascript</strong>. Aprovechando esta circunstancia, crean un campo de formulario que es ocultado con css, con lo cual el usuario normal no lo ve, pero si el bot, que si lo rellena con texto, queda descartado. Pero los spammers aprenden muy rapido, así que la medida no es muy efectiva. Además últimamente empiezan a aparecer personas dedicadas a introducir comentarios spam a mano (de ahí vienen muchos de esos comentarios <a href="http://www.frikipedia.es/friki/HOYGAN">hoygan</a> absurdos que no parecen tener mucho sentido) y que cobraran una miseria en países subdesarrollados, en parte para saltarse los captcha. Por eso su efectividad es muy dudosa. Aunque si puede ser usado combinado con otros módulos spam, para reforzar su eficacia. Es muy poco usado, apenas <strong><em>377 sitios</em></strong> lo emplean. </li> <li><a href="http://drupal.org/project/antispam">Antispam</a>, uno de los mejores módulos antispam para drupal. Con el se puede usar algunos de los mejores servicios antispam externos que hay en la red: <a href="http://akismet.com/">Akismet</a>, <a href="http://antispam.typepad.com/">Typepad</a> y <a href="http://defensio.com/">Defensio</a>. Con él podemos abrir una cuenta en uno de estos servicios y configurar el módulo para emplearlo. Su eficacia es muy elevada, ya que son algunas de los mejores armas disponibles contra el spam. El funcionamiento básico es consultar la base de datos de alguno de estos servicios, muy completas, para comprobar si el comentario u el posteador son probable spam, y bloquearlo en caso de que la probabilidad sea muy elevada. En caso de duda, aparecerá un captcha para descartar bots. Tiene unas gráficas estadísticas muy útiles para comprobar la evolución del problema en nuestro sitio. No es demasiado empleado, estando instalado en unos <strong><em>1.718 sitios</em></strong>. </li> <li><a href="http://drupal.org/project/badbehavior%20">Bad Behavior</a>, otro viejo conocido de las medidas antispam. Este se basa en parte en un análisis heurístico de las peticiones HTTP del bot y comparándolo con las bases de datos que poseen de spambots conocidos. Este hace uso también de las base de datos del <strong>Proyecto Honey Pot</strong>, para reforzar la identificación de spammers. Es bastante eficiente, pero el problema está en que van por detrás siempre de los spammers y a veces se les cuela algún que otro comentario spam. Básicamente porque se basa en que uno reporte los spammers que aún no están en sus bases de datos, y hasta que alguien reporta a un spammer, este puede habernos colado unos cuantos mensajes. Es empleado en unos <strong><em>918 sitios</em></strong> drupal.</li> <li><a href="http://drupal.org/project/httpbl">http:BL</a>, ese se basa enteramente en el <a href="http://www.projecthoneypot.org/">Proyecto Honey Pot</a>. Usa sus bases de datos (<a href="http://es.wikipedia.org/wiki/Lista_negra">DNS blacklist</a>) para prevenir comentarios spam y recolectores de direcciones email. Es eficiente en la misma medida que el anterior, depende de su base de datos, que no es tan completa como las de los servicios que soporta el modulo <strong>Antispam</strong>. Una de las virtudes de este modulo es que bloquea las solicitudes de pagina de aquellas Ips que están en su lista negra, con el consiguiente beneficio que esto reporta para el trafico de nuestro sitio. Permite también el uso de <a href="http://en.wikipedia.org/wiki/Whitelist">whitelists</a> y <a href="http://en.wikipedia.org/wiki/Greylist">greylists</a>. Podría ser uno de los mejores módulos antispam para Drupal si no se colaran más comentarios spam de lo deseable. Actualmente solo<strong><em>443 sitios</em></strong> emplean este modulo.</li> <li><a href="http://drupal.org/project/phpids">PHPIDS</a>, esta emplea una aproximación al problema diferente. Emplea un sistema de detección de intrusos desarrollado y mantenido por <a href="http://php-ids.org/">PHPIDS</a>. Este no solo detecta ataques de spam, si no que también otro tipo de ataques maliciosos al sitio, como <a href="http://es.wikipedia.org/wiki/XSS">XSS (cross site scripting)</a>, <a href="http://es.wikipedia.org/wiki/Inyeccion_SQL">inyecciones sql</a>, DoS, etc. El problema es que arroja demasiados falsos positivos y hemos de ir afinando la detección poco a poco, lo cual puede llegar a ser bastante tedioso. Se puede usar conjuntamente con otros módulos antispam, pero normalmente este bloqueará el ataque antes de que el otro se percate. Lo malo, claro, es que hasta que no esté completamente afinado, a los usuarios les puede dar mucho la lata ante comentarios completamente inocuos. También puede llegar a generar unos logs muy extensos que pueden incrementar bastante nuestra base de datos. Puede ser muy útil para aquellos sitios en los que los ataques van más allá del simple spam. Tampoco es muy empleado, solo <strong><em>361 sitios</em></strong> lo tienen implantado.</li> <li><a href="http://drupal.org/project/mollom">Mollom</a>, uno de los últimos en llegar, pero lo ha hecho arrasando, en dos años ha conseguido que ya sea empleado en <strong>23.983 sitios</strong> drupal. Esto se debe en parte a que uno de los co-autores es el creador de <strong>Drupal</strong>, <a href="http://buytaert.net/">Dries Buytaert</a>. <a href="http://mollom.com/">Mollom</a> es un servicio web en la misma linea que <strong>Askimet</strong> o <strong>Defensio</strong>, con una base de datos de usuarios en la que aparte de spammers, se registran reputaciones de usuario en función de parámetros como comentarios ofensivos, comentarios de "baja-calidad" (hoygans), comentarios off-topic, etc según como nosotros lo reportemos a <strong>Mollom</strong>. Es decir que nos ayuda también a mejorar la calidad de nuestro sitio filtrando también a usuarios con baja reputación en función de los parametros que nosotros marquemos. Esto desde luego es un punto a favor del servicio, que nos permite matar dos pajaros de un tiro. El servicio analiza el texto del mensaje, y si es spam, lo bloquea y en caso de dudas mostrara un captcha como el de la imagen (menos del 2% de las ocasiones). Además todo el código es opensource, tanto el del modulo como el de la API de <strong>Mollom</strong> y hay disponibles módulos para otros gestores de contenidos como <strong>Wordpress</strong>, <a href="http://www.joomla.org/">Joomla</a> o <a href="http://radiantcms.org/">Radiant</a> y librerías para múltiples lenguajes (Java, PHP, Ruby, Python, Perl, .Net, ...).</li> </ul> <p>Después de analizar las posibilidades y probar unos cuantos módulos (<strong>Antispam</strong>, <strong>Bad Behavior</strong>, <strong>http:BL</strong>, <strong>PHPIDS</strong> y <strong>Mollom</strong>) llegué a valorar que las dos mejores soluciones en mi caso eran <strong>Antispam</strong> Y <strong>Mollom</strong>. Aunque <strong>PHPIDS</strong> y <strong>http:BL</strong> tenían algunas características únicas que echaba de menos en ellos. Después de probar durante unas semanas tanto <strong>Antispam</strong> como <strong>Mollom</strong>, observe que el indice de fallos de <strong>Mollom</strong> era mucho menor y además era más transparente al usuario, mostrando el captcha en menos ocasiones. Si, <strong>Captcha</strong> es la opción más socorrida por la mayoría de los usuarios de <strong>Drupal</strong>, en cuanto que es la que menos molesta al webmaster, claro, pero le traspasa la molestia al usuario. Yo odio directamente los captcha, no los soporto, y he pasado de utilizar alguna web por ellos. <strong>Mollom</strong> tiene la ventaja de reducir esta molestia a la minima expresión, por lo que el 98% de los usuarios de la web ni siquiera se darán cuenta de que en ella funciona un sistema antispam, que es lo que buscaba desde el principio, un servicio efectivo y transparente.</p> <p><strong>Mollom</strong> era pues, la opción elegida y la que está funcionando en este sitio desde entonces.</p> <p style="text-align: center;"><img src="pictures/captcha.png" alt="Ejemplo de captcha de Mollom" title="Ejemplo de captcha de Mollom" height="114" width="600" /></p> <p>Ejemplo de captcha generado por Mollom</p> <h2 id="la_soluci+n_definitiva_mollom__ban_drupal_spammerspy">La solución definitiva, Mollom + ban_drupal_spammers.py</h2> <p><strong>Aunque Mollom funciona de manera muy efectiva, bloqueando aprox. el 99,98% (en deaparatos.com) de los mensajes spam</strong>, esto no impide que los atacantes sigan intentando una y otra vez colar su spam en el sitio. Esto nos lleva a que las páginas se cargan una y otra vez, consumiendo ancho de banda, ya que <strong>Mollom</strong> actúa a posteriori, cuando se envía el comentario, no antes de cargar la página, lo que es el funcionamiento normal de estos sistemas antispam. Y además los spammers tienen cierta inclinación a intentar introducir el spam en las paginas más populares, las que suelen tener más comentarios y por lo tanto de mayor peso por lo general. Basta con decir que el ancho de banda medio generado por cada uno de estos ataques en este sitio ha sido de 146Kb.<br /> </p> <p>Y es en este aspecto donde echaba de menos una de las características de <strong>http:BL</strong>, bloquear el acceso al sitio a los que están en su lista negra. Empecé entonces a darle vueltas a la manera de implementar esta característica en mi sitio, pero pronto me di cuenta de dos cosas:</p> <ul> <li>No quería hacer una consulta a projecthoneypot.org cada vez que alguien accediera al sitio, por evidentes mermas en el rendimiento del sitio.</li> <li>No quería tampoco tener una lista negra local que se alimentara periódicamente de projecthoneypot.org, porque no quería tener que comprobar miles de ips que probablemente nunca accederían a mi sitio.</li> </ul> <p>La solución entonces pasaba por bloquear solo a los que ya hubieran ejecutado un ataque de spam contra el sitio y que hubieran sido bloqueados al menos una vez por <strong>Mollom</strong>, de modo que en los sucesivos ataques fueran rechazados antes siquiera de cargar la página.</p> <p>Hay una forma de hacer esto de forma manual en <strong>Drupal</strong>, simplemente hay que añadir las ips de los spammers a través de las <em>reglas de acceso</em> en el menú de <em>Administración</em>. Claro que el método es evidentemente tedioso y aparatoso, comprobar las ips atacantes e ir añadiéndolas una por una a través del formulario. Tenía que hacerse de una manera automatizada.</p> <p>La primera idea y más evidente era modificar el modulo <strong>Mollom</strong> para lograr esto, pero no me gusta PHP y procuro evitarlo, además quería un prototipo rápido para evaluar la eficacia de la solución y su repercusión en el ancho de banda, así que todo empezó con un sencillo script en <strong>python</strong>. Pronto me di cuenta de que <strong>Mollom</strong> registraba las ips de todos los atacantes que bloqueaba en el registro de eventos de <strong>Drupal</strong> (la tabla <em>watchdog</em> del modulo opcional <em>Database logging</em>), y que alguna de ellas tenía hasta 30 entradas diferentes en el registro. Y como <strong>Drupal</strong> incorpora el método que citaba antes para banear IPs, lo único necesario era añadir estas IPs a la tabla <em>access</em>.</p> <p style="text-align: center;"><img src="pictures/banned.png" alt="Ejemplo de pagina de ip bloqueada por Drupal" title="Ejemplo de pagina de ip bloqueada por Drupal" height="30" width="233" /></p> <p>Este es un ejemplo de la pàgina que se encontraria un atacante de spam bloqueado a través de la tabla <em>access</em> en Drupal</p> <p>Ahora bien, si añadimos automáticamente estas IPs, llegara un momento en que tendremos varias miles de ellas, y el rendimiento de la página se vera afectado, al tener que comprobar todas estas ips cada vez que alguien accede a la página. Además hay que tener en cuenta que algunas de estas IPs tendrán como origen a un usuario que teniendo el ordenador o router infectado por un <a href="http://es.wikipedia.org/wiki/Rootkit">rootkit</a>/<a href="http://es.wikipedia.org/wiki/Troyano_%28inform%C3%A1tica%29">troyano</a>, pertenezca a una <a href="http://es.wikipedia.org/wiki/Botnet">Botnet</a> sin saberlo. Es posible que estos usuarios acaben limpiando de <a href="http://es.wikipedia.org/wiki/Malware">malware</a> su equipo y en un momento determinado quieran acceder legítimamente al sitio, por lo que no deberían estar bloqueados de por vida. Esto lo solucioné en el script rotando las IPs al llegar a umbral determinado, marcado por el número máximo de las IPs que deseemos almacenar en esta tabla. Al llegar a este número máximo, se borra un porcentaje de IPs, eligiendo siempre a las más antiguas. En estos momentos, en función del rendimiento y tiempo que quiero que permanezcan en la tabla, tengo este valor establecido en unas 2000 IPs. Para controlar la fecha en que fueron introducidas cada una de las ips en la tabla, modifico la tabla access, añadiéndole un campo <em>timestamp</em>.</p> <p>Como se pudo ver al principio del articulo, la efectividad del script es muy elevada y a día de hoy sigo con este método, con un script que ha evolucionado varias veces desde entonces y que se adapta perfectamente a mis necesidades. Los picos que se pueden ver en el primer gráfico del spam bloqueado por el modulo <strong>Mollom</strong>, se deben precisamente a los breves periodos de tiempo en los que por una u otra razón el script no estaba funcionando.</p> <p>El porqué del fantástico ahorro de ancho de banda se puede explicar con la anterior imagen, que es un ejemplo de la página que se encontraría un atacante bloqueado por <strong>ban_drupal_spammers.py</strong>. <strong>Esta página tiene un peso ridículo de entre 33 y 39 bytes, del orden de unas 4000 veces menos que el peso medio de 146 Kilobytes por página del trafico generado por los spammers</strong>.</p> <p>Este script se puede ejecutar en remoto, para hostings compartidos que no pueden correr scripts en <strong>python</strong> pero si permiten acceso remoto a la base de datos en MySQL, como mi anterior <a href="http://www.hostsuar.com/">hosting</a> (quede muy satisfecho). Pero también puede ser ejecutado de manera local, en hosting compartidos (que soporten python), en <a href="http://es.wikipedia.org/wiki/Servidor_virtual_privado">VPS</a> y en servidores dedicados. No muchos hostings compartidos permiten la ejecución de scripts en python, ni siquiera ssh o acceso remoto a la BDD. Afortunadamente, mi hosting actual, <strong><a href="http://www.webfaction.com/?affiliate=joedicastro">Webfaction</a></strong>, me permite todas esas posibilidades y no es ningún un problema. De hecho es el mejor hosting compartido que haya probado nunca y uno de los mejores del mercado, porque su manera de trabajar es única y es lo más parecido a un VPS, pero con una facilidad para administrar las tareas más cotidianas apabullante. Eso si, es distinto a todos los demás y necesita uno adaptarse a su manera de hacer las cosas, pero luego ya no quieres saber nada de otros hosting compartidos. Si además quieres trabajar con ruby o python, pocos puede competir con su flexibilidad, lo que me hizo decidirme por él.</p> <h2 id="el_script_ban_drupal_spammerspy">El script, ban_drupal_spammers.py</h2> <p>El script (siempre la versión más actualizada), los ficheros auxiliares y las instrucciones de como emplearlos, pueden ser encontrados en mi repositorio que se encuentra alojado en <a href="http://github.com/joedicastro/ban-drupal-spammers">github</a>.Y donde tambien se puede encontrar el script python que empleo para recoger los datos que se muestran en el mapa de este árticulo.</p> <p>El código de <strong>ban_drupal_spammers.py</strong> es el siguiente:</p> <div class="codehilite"><pre><span class="c">#!/usr/bin/env python</span> <span class="c"># -*- coding: utf8 -*-</span> <span class="sd">&quot;&quot;&quot;</span> <span class="sd"> ban drupal spammers.py: ban spammers in Drupal with Mollom&#39;s aid</span> <span class="sd">&quot;&quot;&quot;</span> <span class="c">#===============================================================================</span> <span class="c"># This Script uses the Mollom reports in Drupal for ban spammers&#39; ips and</span> <span class="c"># reduce the bandwith usage in the website.</span> <span class="c">#===============================================================================</span> <span class="c">#===============================================================================</span> <span class="c"># Copyright 2010 joe di castro &lt;joe@joedicastro.com&gt;</span> <span class="c">#</span> <span class="c"># This program is free software: you can redistribute it and/or modify</span> <span class="c"># it under the terms of the GNU General Public License as published by</span> <span class="c"># the Free Software Foundation, either version 3 of the License, or</span> <span class="c"># (at your option) any later version.</span> <span class="c">#</span> <span class="c"># This program is distributed in the hope that it will be useful,</span> <span class="c"># but WITHOUT ANY WARRANTY; without even the implied warranty of</span> <span class="c"># MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the</span> <span class="c"># GNU General Public License for more details.</span> <span class="c">#</span> <span class="c"># You should have received a copy of the GNU General Public License</span> <span class="c"># along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.</span> <span class="c">#</span> <span class="c">#===============================================================================</span> <span class="n">__author__</span> <span class="o">=</span> <span class="s">&quot;joe di castro - joe@joedicastro.com&quot;</span> <span class="n">__license__</span> <span class="o">=</span> <span class="s">&quot;GNU General Public License version 3&quot;</span> <span class="n">__date__</span> <span class="o">=</span> <span class="s">&quot;15/05/2010&quot;</span> <span class="n">__version__</span> <span class="o">=</span> <span class="s">&quot;0.52&quot;</span> <span class="k">try</span><span class="p">:</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">base64</span> <span class="kn">import</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="nn">MySQLdb</span> <span class="kn">import</span> <span class="nn">pygeoip</span> <span class="kn">import</span> <span class="nn">logger</span> <span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span> <span class="c"># Checks the installation of the necessary python modules</span> <span class="k">print</span><span class="p">((</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s">&quot;An error found importing one module:&quot;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">exc_info</span><span class="p">()[</span><span class="mi">1</span><span class="p">]),</span> <span class="s">&quot;You need to install it&quot;</span><span class="p">,</span> <span class="s">&quot;Exit...&quot;</span><span class="p">]))</span> <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="k">def</span> <span class="nf">connect_db</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">pass_</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">3306</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Connect to MySQL database.&quot;&quot;&quot;</span> <span class="k">try</span><span class="p">:</span> <span class="n">data_base</span> <span class="o">=</span> <span class="n">MySQLdb</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span> <span class="n">user</span><span class="o">=</span><span class="n">user</span><span class="p">,</span> <span class="n">passwd</span><span class="o">=</span><span class="n">pass_</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="n">db</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">,</span> <span class="n">client_flag</span><span class="o">=</span><span class="mi">65536</span><span class="p">)</span> <span class="c"># flag 65536 is to allow multiple statements in a single string, equals</span> <span class="c"># to CLIENT_MULTI_STATEMENTS</span> <span class="k">except</span> <span class="n">MySQLdb</span><span class="o">.</span><span class="n">OperationalError</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">&quot;Database connection fails, check that you gave the right &quot;</span> <span class="s">&quot;credentials to access the database{0}Exit...&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">))</span> <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="k">return</span> <span class="n">data_base</span> <span class="k">def</span> <span class="nf">select</span><span class="p">(</span><span class="n">curs</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Runs a SQL SELECT query and returns a tuple as output.&quot;&quot;&quot;</span> <span class="n">curs</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span> <span class="k">return</span> <span class="n">curs</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span> <span class="k">def</span> <span class="nf">alter_table</span><span class="p">(</span><span class="n">curs</span><span class="p">,</span> <span class="n">db_table</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create the aux field in the table if no exists, else do nothing.&quot;&quot;&quot;</span> <span class="n">database_string</span> <span class="o">=</span> <span class="s">&quot;&quot;&quot;</span> <span class="s"> ALTER TABLE {0}</span> <span class="s"> ADD timestamp INT(11) NOT NULL DEFAULT &#39;0&#39;;</span> <span class="s"> &quot;&quot;&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">db_table</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="n">curs</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">database_string</span><span class="p">)</span> <span class="k">return</span> <span class="s">&quot;Aux Field &#39;timestamp&#39; in table &#39;{0}&#39; created.&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">db_table</span><span class="p">)</span> <span class="k">except</span> <span class="n">MySQLdb</span><span class="o">.</span><span class="n">OperationalError</span><span class="p">:</span> <span class="k">print</span> <span class="p">(</span><span class="s">&quot;Can&#39;t create the aux field, seems this exists previously.&quot;</span><span class="p">)</span> <span class="c"># This output is not reported in the log, it will be repetitive.</span> <span class="k">def</span> <span class="nf">ins_qstr</span><span class="p">(</span><span class="n">q_mask</span><span class="p">,</span> <span class="n">q_timestamp</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create a SQL INSERT query string for the given ip.&quot;&quot;&quot;</span> <span class="n">iqstr</span> <span class="o">=</span> <span class="s">&quot;&quot;&quot;</span> <span class="s"> INSERT INTO `access`</span> <span class="s"> (mask, type, status, timestamp)</span> <span class="s"> VALUES (&#39;{0}&#39;, &#39;host&#39;, &#39;0&#39;, {1});{2}</span> <span class="s"> &quot;&quot;&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">q_mask</span><span class="p">,</span> <span class="n">q_timestamp</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">)</span> <span class="k">return</span> <span class="n">iqstr</span> <span class="k">def</span> <span class="nf">del_qstr</span><span class="p">(</span><span class="n">q_timestamp</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create a DELETE query string for the given timestamp.&quot;&quot;&quot;</span> <span class="n">dqstr</span> <span class="o">=</span> <span class="s">&quot;&quot;&quot;</span> <span class="s"> DELETE FROM access</span> <span class="s"> WHERE timestamp=&#39;{0}&#39;;{1}</span> <span class="s"> &quot;&quot;&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">q_timestamp</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">)</span> <span class="k">return</span> <span class="n">dqstr</span> <span class="k">def</span> <span class="nf">ip_and_country</span><span class="p">(</span><span class="n">l_ips</span><span class="p">,</span> <span class="n">geo</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Create the log lines about the ips and their countries.&quot;&quot;&quot;</span> <span class="n">output</span> <span class="o">=</span> <span class="bp">None</span> <span class="k">if</span> <span class="n">l_ips</span><span class="p">:</span> <span class="n">total</span> <span class="o">=</span> <span class="s">&quot;{0} IPs&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">l_ips</span><span class="p">))</span> <span class="n">ips_and_countries</span> <span class="o">=</span> <span class="p">[(</span><span class="n">geo</span><span class="o">.</span><span class="n">country_name_by_addr</span><span class="p">(</span><span class="n">l</span><span class="p">),</span> <span class="n">l</span><span class="p">)</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">l_ips</span><span class="p">]</span> <span class="n">ips</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s">&#39;{0:16} {1}&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">i</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">ips_and_countries</span><span class="p">)])</span> <span class="n">output</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">total</span><span class="p">,</span> <span class="s">&#39;&#39;</span><span class="p">,</span> <span class="n">ips</span><span class="p">])</span> <span class="k">return</span> <span class="n">output</span> <span class="k">def</span> <span class="nf">renew_geoip</span><span class="p">(</span><span class="n">gip_path</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Check if the geoip data file is too old.&quot;&quot;&quot;</span> <span class="n">out_str</span> <span class="o">=</span> <span class="s">&#39;&#39;</span> <span class="n">gz_file</span> <span class="o">=</span> <span class="p">(</span><span class="s">&quot;http://geolite.maxmind.com/download/geoip/database/&quot;</span> <span class="s">&quot;GeoLiteCountry/GeoIP.dat.gz&quot;</span><span class="p">)</span> <span class="n">web_url</span> <span class="o">=</span> <span class="s">&quot;http://www.maxmind.com/app/geolitecountry&quot;</span> <span class="n">geoip_file_date</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">getmtime</span><span class="p">(</span><span class="n">gip_path</span><span class="p">)</span> <span class="k">if</span> <span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">geoip_file_date</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2592000</span><span class="p">:</span> <span class="c"># 2592000s = 30 days</span> <span class="n">out_str</span> <span class="o">+=</span> <span class="p">(</span><span class="s">&quot;Your GeoIP data file* is older than 30 days!{0}{0}&quot;</span> <span class="s">&quot;You can look for a new version in:{0}{1}{0}or{0}{2}{0}{0}&quot;</span> <span class="s">&quot; *{3}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">,</span> <span class="n">gz_file</span><span class="p">,</span> <span class="n">web_url</span><span class="p">,</span> <span class="n">gip_path</span><span class="p">))</span> <span class="k">return</span> <span class="n">out_str</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="sd">&quot;&quot;&quot;main section&quot;&quot;&quot;</span> <span class="c">#===============================================================================</span> <span class="c"># SCRIPT PARAMATERS</span> <span class="c">#===============================================================================</span> <span class="c"># database host, name or ip (&#39;localhost&#39; by default)</span> <span class="n">host</span> <span class="o">=</span> <span class="s">&#39;localhost&#39;</span> <span class="c"># database user name (&#39;root&#39; by default)</span> <span class="n">user</span> <span class="o">=</span> <span class="s">&#39;root&#39;</span> <span class="c"># database password, with a minimum security measure, encoded by base64</span> <span class="c"># (&#39;password&#39; by default)</span> <span class="n">password</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64decode</span><span class="p">(</span><span class="s">&#39;cGFzc3dvcmQ=&#39;</span><span class="p">)</span> <span class="c"># database name (&#39;database&#39; by default)</span> <span class="n">database</span> <span class="o">=</span> <span class="s">&#39;database&#39;</span> <span class="c"># path to geolocation data file GeoIP.dat</span> <span class="n">geoip_path</span> <span class="o">=</span> <span class="s">&#39;/your/path/to/file/GeoIP.dat&#39;</span> <span class="c"># mail server, smtp protocol, to send the log (&#39;localhost&#39; by default)</span> <span class="n">smtp_server</span> <span class="o">=</span> <span class="s">&#39;localhost&#39;</span> <span class="c"># sender&#39;s email address (&#39;&#39; by default)</span> <span class="n">from_addr</span> <span class="o">=</span> <span class="s">&#39;&#39;</span> <span class="c"># a list of receiver(s)&#39; email addresses ([&#39;&#39;] by default)</span> <span class="n">to_addrs</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;&#39;</span><span class="p">]</span> <span class="c"># smtp server user (&#39;&#39; by default)</span> <span class="n">smtp_user</span> <span class="o">=</span> <span class="s">&#39;&#39;</span> <span class="c"># smtp server password, with a minimum security measure, encoded by base64</span> <span class="c"># (&#39;password&#39; by default)</span> <span class="n">smtp_pass</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64decode</span><span class="p">(</span><span class="s">&#39;cGFzc3dvcmQ=&#39;</span><span class="p">)</span> <span class="c"># set the perfomace threshold (number of banned ips) for you site</span> <span class="n">threshold</span> <span class="o">=</span> <span class="mi">2000</span> <span class="c">#===============================================================================</span> <span class="c"># END PARAMETERS</span> <span class="c">#===============================================================================</span> <span class="c"># Initialize the log</span> <span class="n">log</span> <span class="o">=</span> <span class="n">logger</span><span class="o">.</span><span class="n">Logger</span><span class="p">()</span> <span class="c"># log the header</span> <span class="n">url</span> <span class="o">=</span> <span class="s">&#39;http://joedicastro.com&#39;</span> <span class="n">connected</span> <span class="o">=</span> <span class="s">&#39;Connected to {0} in {1} as {2}&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">database</span><span class="p">,</span> <span class="n">host</span><span class="p">,</span> <span class="n">user</span><span class="p">)</span> <span class="n">log</span><span class="o">.</span><span class="n">header</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">connected</span><span class="p">)</span> <span class="c"># log the start time</span> <span class="n">log</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&#39;Start Time&#39;</span><span class="p">)</span> <span class="c"># log the warning about old geolocation data file</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&#39;The GeoIp.dat file is old&#39;</span><span class="p">,</span> <span class="n">renew_geoip</span><span class="p">(</span><span class="n">geoip_path</span><span class="p">))</span> <span class="c"># connect to database, create the cursors &amp; initialize the geolocation info</span> <span class="n">mysql_db</span> <span class="o">=</span> <span class="n">connect_db</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">database</span><span class="p">)</span> <span class="n">cursor</span> <span class="o">=</span> <span class="n">mysql_db</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span> <span class="n">dict_cursor</span> <span class="o">=</span> <span class="n">mysql_db</span><span class="o">.</span><span class="n">cursor</span><span class="p">(</span><span class="n">MySQLdb</span><span class="o">.</span><span class="n">cursors</span><span class="o">.</span><span class="n">DictCursor</span><span class="p">)</span> <span class="n">gip</span> <span class="o">=</span> <span class="n">pygeoip</span><span class="o">.</span><span class="n">GeoIP</span><span class="p">(</span><span class="n">geoip_path</span><span class="p">)</span> <span class="c"># optimize the database (instead a cron task in the server)</span> <span class="n">all_tables</span> <span class="o">=</span> <span class="p">[</span><span class="n">tabl</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">tabl</span> <span class="ow">in</span> <span class="n">select</span><span class="p">(</span><span class="n">cursor</span><span class="p">,</span> <span class="s">&quot;SHOW TABLES&quot;</span><span class="p">)]</span> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">&#39;OPTIMIZE TABLE {0}&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="s">&#39;, &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">all_tables</span><span class="p">)))</span> <span class="c"># Adds the timestamp field to the &#39;access&#39; table if no exists</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&#39;New aux table field created&#39;</span><span class="p">,</span> <span class="n">alter_table</span><span class="p">(</span><span class="n">cursor</span><span class="p">,</span> <span class="s">&#39;access&#39;</span><span class="p">))</span> <span class="c"># Query the database and obtain the result. We collect the &#39;access&#39; table</span> <span class="c"># ips and ips from spammers reported by Mollom in &#39;watchdog&#39; table</span> <span class="c"># access = ({&#39;timestamp&#39;:timestamp, &#39;mask&#39;: &#39;ip&#39;}, ...)</span> <span class="c"># mollom = ({&#39;timestamp&#39;:timestamp, &#39;mask&#39;: &#39;ip&#39;}, ...)</span> <span class="n">access</span> <span class="o">=</span> <span class="n">select</span><span class="p">(</span><span class="n">dict_cursor</span><span class="p">,</span> <span class="s">&quot;&quot;&quot;SELECT mask, timestamp FROM access&quot;&quot;&quot;</span><span class="p">)</span> <span class="n">mollom</span> <span class="o">=</span> <span class="n">select</span><span class="p">(</span><span class="n">dict_cursor</span><span class="p">,</span> <span class="s">&quot;&quot;&quot;SELECT hostname as mask, timestamp</span> <span class="s"> FROM `watchdog`</span> <span class="s"> WHERE `type` LIKE &#39;%mollom%&#39;</span> <span class="s"> AND `message` LIKE &#39;</span><span class="si">%s</span><span class="s">pam:%&#39;&quot;&quot;&quot;</span><span class="p">)</span> <span class="c"># From the &#39;access&#39; ips, select the ips blocked by this script from Mollom,</span> <span class="c"># discarding those introduced through the Drupal administration interface</span> <span class="c"># from_access = {&#39;ip&#39;:timestamp, ...}</span> <span class="n">from_access</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">for</span> <span class="n">a_row</span> <span class="ow">in</span> <span class="n">access</span><span class="p">:</span> <span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="n">a_row</span><span class="p">[</span><span class="s">&#39;timestamp&#39;</span><span class="p">]):</span> <span class="n">from_access</span><span class="p">[</span><span class="n">a_row</span><span class="p">[</span><span class="s">&#39;mask&#39;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">a_row</span><span class="p">[</span><span class="s">&#39;timestamp&#39;</span><span class="p">]</span> <span class="c"># Here we select the ips that Mollom reported, if there are multiple</span> <span class="c"># occurrences of the same ip, we always choose the most recent</span> <span class="c"># from_mollom = {&#39;ip&#39;:timestamp, ...}</span> <span class="n">from_mollom</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">for</span> <span class="n">m_row</span> <span class="ow">in</span> <span class="n">mollom</span><span class="p">:</span> <span class="k">if</span> <span class="n">m_row</span><span class="p">[</span><span class="s">&#39;mask&#39;</span><span class="p">]</span> <span class="ow">in</span> <span class="n">from_mollom</span><span class="o">.</span><span class="n">keys</span><span class="p">():</span> <span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="n">from_mollom</span><span class="p">[</span><span class="n">m_row</span><span class="p">[</span><span class="s">&#39;mask&#39;</span><span class="p">]])</span> <span class="o">&lt;</span> <span class="nb">int</span><span class="p">(</span><span class="n">m_row</span><span class="p">[</span><span class="s">&#39;timestamp&#39;</span><span class="p">]):</span> <span class="n">from_mollom</span><span class="p">[</span><span class="n">m_row</span><span class="p">[</span><span class="s">&#39;mask&#39;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">m_row</span><span class="p">[</span><span class="s">&#39;timestamp&#39;</span><span class="p">]</span> <span class="k">else</span><span class="p">:</span> <span class="n">from_mollom</span><span class="p">[</span><span class="n">m_row</span><span class="p">[</span><span class="s">&#39;mask&#39;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">m_row</span><span class="p">[</span><span class="s">&#39;timestamp&#39;</span><span class="p">]</span> <span class="c"># Now, from these ips, select the IPs of spammers that were not already</span> <span class="c"># banned and generate queries to insert into the &#39;access&#39; table. It&#39;s</span> <span class="c"># necessary to check if some of ips reported through Mollom didn&#39;t be</span> <span class="c"># already banned, because of how the Drupal&#39;s event log works. The optional</span> <span class="c"># core module &quot;Database logging&quot; (which must be enabled to run his script)</span> <span class="c"># is deleting records by the tail (into the &#39;watchdog&#39; table) on each cron</span> <span class="c"># run, according to a maximum limit set in the admin menu. This limit may be</span> <span class="c"># 100, 1000, 10000, 100000, 1000000 records, as determined in the &quot;Loggin</span> <span class="c"># and alerts -&gt; Database logging&quot; menu. Then depending on the record limit</span> <span class="c"># set in the &#39;watchdog&#39; table, the frequency with which you run the cron job</span> <span class="c"># and how often you run this script, it&#39;s very likely that in the previous</span> <span class="c"># query we have returned a number of ips that have not yet eliminated from</span> <span class="c"># the log (&#39;watchdog&#39;), but we have already added to the table of bannedd</span> <span class="c"># ips (&#39;access&#39;). This will avoid duplicate ips on table &#39;access&#39;</span> <span class="c"># ins_ips = [&#39;ip0&#39;, &#39;ip1&#39;, ...]</span> <span class="n">ins_ips</span> <span class="o">=</span> <span class="p">[</span><span class="n">f_ip</span> <span class="k">for</span> <span class="n">f_ip</span> <span class="ow">in</span> <span class="n">from_mollom</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span> <span class="k">if</span> <span class="n">f_ip</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">from_access</span><span class="p">]</span> <span class="n">query_str</span> <span class="o">=</span> <span class="s">&#39;&#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ins_qstr</span><span class="p">(</span><span class="n">i_ip</span><span class="p">,</span> <span class="n">from_mollom</span><span class="p">[</span><span class="n">i_ip</span><span class="p">])</span> <span class="k">for</span> <span class="n">i_ip</span> <span class="ow">in</span> <span class="n">ins_ips</span><span class="p">)</span> <span class="c"># number of banned ips through this script</span> <span class="n">banned_ips</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">from_access</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">ins_ips</span><span class="p">)</span> <span class="c"># number of banned ips through Drupal administration interface</span> <span class="n">drupal_banned_ips</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">access</span><span class="p">)</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">from_access</span><span class="p">)</span> <span class="c"># After a certain number of records in the table &#39;access&#39;, the website&#39;s</span> <span class="c"># perfomance deteriorates and from an even larger number, the behavior of</span> <span class="c"># Drupal just become erratic. In the case of the site on which to run this</span> <span class="c"># script, we see a clear loss of performance from the 3000 records and</span> <span class="c"># becomes erratic over 5000. To avoid this unpleasant side effect, and</span> <span class="c"># that cure don&#39;t be worse than the disease, I set a performance threshold</span> <span class="c"># in 2000 records, from which records were removed from the table. If the</span> <span class="c"># number of rows is greater than the performance threshold, we proceed to</span> <span class="c"># calculate the ips to remove, selecting the oldest. The number of ips to</span> <span class="c"># delete will be at least the 30% of &quot;from_access&quot;. Just delete records</span> <span class="c"># inserted through this script, never the inserted via Drupal admin</span> <span class="c"># interface</span> <span class="n">trigger</span> <span class="o">=</span> <span class="nb">bool</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">access</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">threshold</span><span class="p">)</span> <span class="c"># perfomance threshold</span> <span class="n">del_ips</span><span class="p">,</span> <span class="n">latest</span> <span class="o">=</span> <span class="p">[],</span> <span class="mi">0</span> <span class="c"># ips to delete (if trigger) &amp; latest ip&#39;s date</span> <span class="k">if</span> <span class="n">trigger</span><span class="p">:</span> <span class="c"># Now we&#39;ll group the ips by date. Use the object collections.defauldict</span> <span class="c"># to group the ips in a dictionary of lists (values) of ips by date</span> <span class="c"># (keys)</span> <span class="c"># ips_by_time = {timestamp:[&#39;ip0&#39;, ..], ...}</span> <span class="n">ips_by_time</span> <span class="o">=</span> <span class="n">collections</span><span class="o">.</span><span class="n">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span> <span class="k">for</span> <span class="n">fa_ip</span> <span class="ow">in</span> <span class="n">from_access</span><span class="p">:</span> <span class="n">ips_by_time</span><span class="p">[</span><span class="n">from_access</span><span class="p">[</span><span class="n">fa_ip</span><span class="p">]]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">fa_ip</span><span class="p">)</span> <span class="c"># We selected the oldest ips to have a number of them greater than or</span> <span class="c"># equal to 30% of blocked by this script</span> <span class="k">for</span> <span class="n">ips_date</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">ips_by_time</span><span class="o">.</span><span class="n">keys</span><span class="p">()):</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">del_ips</span><span class="p">)</span> <span class="o">&lt;</span> <span class="p">((</span><span class="nb">len</span><span class="p">(</span><span class="n">from_access</span><span class="p">)</span> <span class="o">*</span> <span class="mi">30</span><span class="p">)</span> <span class="o">/</span> <span class="mi">100</span><span class="p">):</span> <span class="n">query_str</span> <span class="o">+=</span> <span class="n">del_qstr</span><span class="p">(</span><span class="n">ips_date</span><span class="p">)</span> <span class="c"># delete by date, less queries</span> <span class="k">for</span> <span class="n">d_ip</span> <span class="ow">in</span> <span class="n">ips_by_time</span><span class="p">[</span><span class="n">ips_date</span><span class="p">]:</span> <span class="n">del_ips</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">d_ip</span><span class="p">)</span> <span class="n">banned_ips</span> <span class="o">-=</span> <span class="mi">1</span> <span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="n">ips_date</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">latest</span><span class="p">:</span> <span class="n">latest</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">ips_date</span><span class="p">)</span> <span class="n">latest</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">&#39;%A </span><span class="si">%x</span><span class="s">&#39;</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">localtime</span><span class="p">(</span><span class="n">latest</span><span class="p">))</span> <span class="c"># log spammers&#39; ips deleted from the table</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Spammers&#39; Ips deleted&quot;</span><span class="p">,</span> <span class="n">ip_and_country</span><span class="p">(</span><span class="n">del_ips</span><span class="p">,</span> <span class="n">gip</span><span class="p">))</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Newest date of deleted IPs&quot;</span><span class="p">,</span> <span class="s">&quot;Date: {0}&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">latest</span><span class="p">))</span> <span class="c"># runs the database query</span> <span class="k">if</span> <span class="n">query_str</span><span class="p">:</span> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">query_str</span><span class="p">)</span> <span class="c"># close database cursors</span> <span class="n">cursor</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> <span class="n">dict_cursor</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> <span class="c"># log spammers&#39; ips inserted into the table</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&quot;Spammers&#39; IPs inserted&quot;</span><span class="p">,</span> <span class="n">ip_and_country</span><span class="p">(</span><span class="n">ins_ips</span><span class="p">,</span> <span class="n">gip</span><span class="p">))</span> <span class="c"># log total banned ips by origin</span> <span class="n">log</span><span class="o">.</span><span class="n">list</span><span class="p">(</span><span class="s">&#39;Banned IPs&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s">&#39;Mollom: </span><span class="si">%d</span><span class="s"> IPs&#39;</span> <span class="o">%</span> <span class="n">banned_ips</span><span class="p">,</span> <span class="s">&#39;Drupal: </span><span class="si">%d</span><span class="s"> IPs&#39;</span> <span class="o">%</span> <span class="n">drupal_banned_ips</span><span class="p">])</span> <span class="c"># log the end time</span> <span class="n">log</span><span class="o">.</span><span class="n">time</span><span class="p">(</span><span class="s">&#39;End Time&#39;</span><span class="p">)</span> <span class="c"># send the log by email</span> <span class="n">log</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="s">&#39;Ban Drupal Spammers. Ins: {0} Del: {1}&#39;</span><span class="o">.</span> <span class="n">format</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">ins_ips</span><span class="p">),</span> <span class="nb">len</span><span class="p">(</span><span class="n">del_ips</span><span class="p">)),</span> <span class="n">send_from</span><span class="o">=</span><span class="n">from_addr</span><span class="p">,</span> <span class="n">dest_to</span><span class="o">=</span><span class="n">to_addrs</span><span class="p">,</span> <span class="n">mail_server</span><span class="o">=</span><span class="n">smtp_server</span><span class="p">,</span> <span class="n">server_user</span><span class="o">=</span><span class="n">smtp_user</span><span class="p">,</span> <span class="n">server_pass</span><span class="o">=</span><span class="n">smtp_pass</span><span class="p">)</span> <span class="c"># write the log to a file</span> <span class="n">log</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="n">main</span><span class="p">()</span> </pre></div> <br /> <hr /> <h2 id="comentarios_realizados_anteriormente_en_drupal">Comentarios realizados anteriormente en Drupal</h2> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <a href="http://inseguridad.org/"><img src="pictures/avtr_jbone.png" height=28 width=28 alt="avatar" title="avatar de bjone"/></a></div> <h3 id="muy_interesante">Muy interesante</h3> <p>por <a href="http://inseguridad.org/">bjone</a> el Jue, 14/10/2010 - 11:40</p> <p>muy interesante... voy a probar el mollom... gracias por la información.</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <img src="pictures/avtr_anonimo.png" height=28 width=28 alt="avatar" title="avatar de anónimo"/></div> <h3 id="preguntita">Preguntita</h3> <p>por Anónimo el Mié, 27/10/2010 - 14:00</p> <p>Estoy armando mi sitio, que poseerá foro y tendré lo que entendí tu llamas hosting compartido. Es decir, me alquilaran espacio de hosting. La pregunta es: ¿las precauciones contra el spam en mi sitio deberán correr exclusivamente por mi cuenta o parte de la pelea la lleva el administrador de hospedaje?</p> <p>Gracias.</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <img src="pictures/avtr_anonimo.png" height=28 width=28 alt="avatar" title="avatar de anónimo"/></div> <h3 id="gracias_termin+_de_leer_el">Gracias, terminé de leer el</h3> <p>por Anónimo el Mié, 27/10/2010 - 14:03</p> <p>Gracias, terminé de leer el artículo y me respondí solo :)</p> <p>Si, parece que deberé hacerme cargo activamente :(.</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <a href="http://joedicastro.com"><img src="pictures/avtr_joedicastro.png" height=28 width=28 alt="avatar" title="avatar de joedicastro"/></a></div> <h3 id="si_efectivamente_as+_es">Si, efectivamente así es,</h3> <p>por <a href="http://joedicastro.com">joe di castro</a> el Mié, 27/10/2010 - 20:04</p> <p>Si, efectivamente así es, pero además es así también en los servidores administrados y en los servidores dedicados.</p> <p>A lo sumo se dedican a administrar el hard, el sistema operativo y el sistema base para la web (Apache, Mysql/PostgreSQL, NGINX, PHP, ...). Pero en cuanto a la aplicación web en si misma y todo lo que a ella atañe, es la parte que te toca. Luego dependiendo de según donde acabes teniendo el hosting, en una situación determinada -de un ataque a la web por ejemplo- pueden desde echarte una mano de buena fe hasta exigirte que lo arregles o te cierran la cuenta (si no te la cierran directamente). Depende de con quien des y de la circunstancia que se dé, en la mayoría de hostings compartidos no se paran mucho a dar soporte a este tipo de situaciones, y si a penalizar a los que no gestionan correctamente sus sitios.</p> <p>De todos modos, si eliges un buen sistema de foros y aplicas un buen sistema antispam, no deberías tener excesivos problemas y acabaras aprendiendo mucho por el camino. La mayoría de los problemas vienen por la desidia y la poca preocupación de los administradores de webs por estos temas.</p> <p>Saludos y suerte con el foro.</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <a href="http://sigt.net/"><img src="pictures/avtr_armonth.png" height=28 width=28 alt="avatar" title="avatar de armonth"/></a></div> <h3 id="sobre_el_spamicide">Sobre el Spamicide</h3> <p>por <a href="http://sigt.net/archivo/sistema-antispam-del-campo-oculto-para-wordpress.xhtml">Armonth</a> el Sáb, 30/10/2010 - 19:59 </p> <p>Buenas, yo ese sistema lo conocía por el de "campo oculto" y lo comenté hace ya más de 3 años en SigT (te he enlazado mi nombre al artículo) con la implementación.</p> <p>Un detalle que cabe mencionar es que es mucho más efectivo poner como campo "te echo atrás por spammer" el campo correspondiente a "nombre" originalmente en la implementación de WordPress (author) y poner un nuevo author que no poner un campo nuevo a ver si lo rellenan.</p> <p>La mayoría de scripts para spamear no están hechos para rellenar todos los campos, están hechos para rellenar el nombre (author), email, url y comment... ignorando otros campos...</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <a href="http://joedicastro.com"><img src="pictures/avtr_joedicastro.png" height=28 width=28 alt="avatar" title="avatar de joedicastro"/></a></div> <h3 id="si_desde_luego_es_bastante">Si, desde luego es bastante</h3> <p>por <a href="http://joedicastro.com">joe di castro</a> el Sáb, 30/10/2010 - 22:05</p> <p>Si, desde luego es bastante más lógico hacerlo de esa manera, engañando doblemente a los spammers. De todos modos el modulo Spamicide te deja renombrar el campo como quieras y se puede hacer pasar por uno de esos campos sin problemas, por lo que se puede hacer lo que comentas.</p> <p>Ya conocía el articulo que enlazas, hace mucho tiempo que te sigo :), aunque los dos estamos muy inactivos últimamente.</p> <p>Saludos</p> <hr /> <div style="float:right; padding:2px; border: 1px solid #ccc; height:28px;"> <a href="http://sigt.net/"><img src="pictures/avtr_armonth.png" height=28 width=28 alt="avatar" title="avatar de armonth"/></a></div> <h3 id="bueno">Bueno</h3> <p>por <a href="http://sigt.net/">Armonth</a> el Sáb, 30/10/2010 - 22:57 </p> <p>Bueno, yo estoy "inactivo" de sigt que no de otro proyecto aún no revelado y que dejé los MMO ;P</p> <div class="footnote"> <hr /> <ol> <li id="fn:1"> <p>Las estadísticas de uso emplean los datos de drupal.org a 12 de Octubre de 2010&#160;<a href="#fnref:1" rev="footnote" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> </ol> </div>joe di castroThu, 14 Oct 2010 01:42:00 +0200http://joedicastro.com/combatir-el-spam-en-drupal.htmldrupalspampythonmysqlmollomscript