15 de enero de 2012

Filtrado por lista negra en WebPy


WebPy es un framework para aplicaciones web escrito en python. Se trata de un framework de código libre y está "licenciado" como dominio público.

En este caso queremos mejorar la seguridad de nuestra aplicación web creando una clase que implemente mecanismos de filtrado por lista negra. Existen múltiples listas negras en internet sobre IPs potencialmente dañinas.

En mi caso he decido para implementar esta funcionalidad con la lista negra suministrada por el proyecto honeypot.

En primer lugar vamos a crear una clase que nos permita hacer consultas sobre esta base de conocimiento.

class HttpBL():
 def __init__(self, key):
  self.key = key
  self._DOMAIN = "dnsbl.httpbl.org"

 def get_info(self, ip, timeout=None):
  partes = ip.split(".")
  #Don't ask private networks
  if partes[0] == "10" or partes[0] == "127":
   return (-1,-1,-1)
  elif partes[0] == "192" and partes[1] == "168":
   return (-1,-1,-1)
  elif partes[0] == "172" and int(partes[1]) >= 16 and int(partes[1]) <= 31:
   return (-1,-1,-1)

  domain_ask = ""
  for parte in partes:
   domain_ask = parte+"."+domain_ask

  domain_ask = self.key+"."+domain_ask+self._DOMAIN
  try:
   if timeout:
    socket.timeout = timeout
   result = socket.gethostbyname(domain_ask)
   result = result.split(".")
   return (int(result[1]), int(result[2]), int(result[3]))
  except socket.gaierror as (error, string):
   if error == 11001 or error == -5:
    return (-1,-1,-1)
   else:
    raise socket.gaierror(error, string)
Como podeis observar, las IPs privadas no se consultan.
Podeis obtener más información sobre el tipo de información que nos provee esta lista desde: http://www.projecthoneypot.org/httpbl_api.php

Por otro lado he implementado una clase que, además de comprobar que quien acceda al servicio no sea potencialmente peligroso, define: unas reglas de acceso directamente en la declaración, la creación de una respuesta OPTIONS con los datos anteriores y una comprovación del dominio desde el que se está acediendo a la aplicación.
class SecureWeb():
 def __init__(self, blkey="", host_name=None, allow_methods=["HEAD","GET","POST","OPTIONS"]):
  self.allow_methods = allow_methods
  self.host_name = host_name
  self.blacklist = HttpBL(blkey)
 def not_allowed(self):
  raise web.Forbidden()
 def wrong_hostname(self):
  self.not_allowed()
 def bad_method(self):
  self.not_allowed()
 def to_honeypot(self, data):
  self.not_allowed()
 def to_search_engine(self):
  pass
 def check_allow(self):
  (last_activity, threat_score, type) = self.backlist.get_info(web.ctx.env["REMOTE_ADDR"])
  if type == 0:
   self.to_search_engine()
  elif type > 0:
   self.to_honeypot((last_activity, threat_score, type)) 
  if self.host_name:
   hostname_ok = False
   for hostname in self.host_name:
    if hostname.lower() == web.ctx.env["SERVER_NAME"].lower():
     hostname_ok = True
     break
   if not hostname_ok:
    self.wrong_hostname()
  if not web.ctx.env["REQUEST_METHOD"].upper() in self.allow_methods:
   self.bad_method()

 def HEAD(self, *extras):
  self.check_allow()
 def GET(self, *extras):
  self.check_allow()
 def POST(self, *extras):
  self.check_allow()
 def PUT(self, *extras):
  self.check_allow()
 def DELETE(self, *extras):
  self.check_allow()
 def TRACE(self, *extras):
  self.check_allow()
 def CONNECT(self, *extras):
  self.check_allow()
 def OPTIONS(self, *extras):
  self.check_allow()
  allow = ""
  for method in self.allow_methods:
   allow += method+","
  allow = allow[:1]
  web.header('Allow', allow)

Como se puede observar en cada tipo de petición HTTP se realiza un chequeo de acceso, y en caso de no validarlo se envia la información a la función correspondiente para poder procesarla de forma adecuada. En el ejemplo todas las funciones acaban llamando a 'not_allowed' que muestra un Forbidden. Pero cada uno de vosotros podeis modificar las funciones 'wrong_hostname' 'bad_method' 'to_honeypot' 'to_search_engine' para que realicen las acciones que estimeis oportunas.