Django: Log and block brute force attempts

Here is a very simple mechanism for wrapping a decorator around your views to protect them against brute force attempts. For instance, if you have a secret file download available only with the right secret (/view/id/secret-hash/), you expose your view to simple brute force attempts.

Simply put, this decorator will log a 404 response object or Http404 exception, count pr. IP and return status=400 and send you an email whenever a new block is put into place.

Model

class IllegalLookup(LogModifications):  
    """Log and block illegal lookups"""  
    created = models.DateTimeField(  
        verbose_name=_(u'created'),  
        auto_now_add = True,  
    )  
    modified = models.DateTimeField(  
        verbose_name=_(u'created'),  
        auto_now = True,  
    )  
    ip_address = models.CharField(  
        max_length=16,  
        null=True,  
        blank=True,  
        verbose_name=_(u'IP address'),  
    )  
    path = models.CharField(  
        max_length=255,  
        null=True,  
        blank=True,  
        verbose_name=_(u'path'),  
        help_text=_(u'First attempted path is always logged'),  
    )  
    count = models.PositiveIntegerField(  
        default=1,  
    )

    @classmethod  
    def log_lookup(cls, ip_address, path):  
        try:  
            now = timezone.now()  
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)  
            lookup = cls.objects.get(ip_address=ip_address,  
                modified__gte=expired)  
            lookup.count += 1  
            lookup.save()  
        except cls.DoesNotExist:  
            # Delete old entries first  
            cls.objects.filter(ip_address=ip_address).delete()  
            lookup = cls.objects.create(ip_address=ip_address,  
                path=path)  
            lookup.save()

    @classmethod  
    def is_blocked(cls, ip_address):  
        try:  
            now = timezone.now()  
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)  
            lookup = cls.objects.get(ip_address=ip_address,  
                modified__gte=expired)  
            if lookup.count == settings.BLOCK_ATTEMPTS:  
                mail_admins("IP blocked", "{0} is now blocked, IllegalLookup id: {1}".format(ip_address, lookup.id))  
            if lookup.count > settings.BLOCK_ATTEMPTS:  
                return True  
        except cls.DoesNotExist:  
            pass  
        return False

Decorator

def log_and_block(func):

    def _log_and_block(request, *args, **kwargs):  
        remote_ip = request.META.get('REMOTE_ADDR', None)  
        if IllegalLookup.is_blocked(remote_ip):  
            return HttpResponse('%s is blocked' % remote_ip,  
                status=400)

        is_404 = False  
        is_exception = False  
        try:  
            return_object = func(request, *args, **kwargs)  
            if return_object.status_code == 404:  
                is_404 = True  
        except Http404:  
            is_404 = True  
            is_exception = True

        if is_404:  
            if remote_ip:  
                IllegalLookup.log_lookup(remote_ip,  
                    request.META.get('PATH_INFO', ''))

            if is_exception:  
                raise





        return return_object

    return _log_and_block

Usage

Now, simply wrap the decorator around your view:

@log_and_block  
def my_view(request, id, secret_hash):  
    object = get_object_or_404(models.MyModel, id=id, secret_hash=secret_hash)