Статьи

Счетчики временного окна с повторным обращением и смягчением атак входа в систему, управляемых ботнетом

В этом сообщении блога представлены подсчет времени и ограничение скорости в Redis. Вы можете применить его для активации логина CAPTCHA на своем сайте только тогда, когда это необходимо. Синтаксис выделенного исходного кода Python приведен в оригинальном сообщении в блоге .

1. О Redis

Redis-малые

Redis — это хранилище ключей и постоянный кеш. Помимо нормальной функциональности get / set, он предлагает более сложные структуры данных, такие как списки, хэши и отсортированные наборы. Если вы знакомы с memcached, подумайте Redis как memcached со стероидами.

Часто Redis используется в целях ограничения скорости . Обычно рецепты ограничения скорости подсчитывают, сколько раз что-то происходит за определенную секунду или определенную минуту. Когда часы переходят на следующую минуту, счетчик ограничения скорости сбрасывается на ноль. Это может быть проблематично, если вы хотите ограничить показатели, когда число обращений за интервал времени интеграции очень низкое. Если вы хотите ограничить пять попаданий в минуту, в одном временном окне вы получаете только один удар, а шесть — в другое, хотя среднее значение за две минуты составляет 3,5.

В этом посте представлен пример Python, как выполнять подсчет на основе скользящего временного окна, чтобы подсчет скорости не возвращался к нулю ни в одной точке, а подсчитывал попадания в течение X секунд в прошлое. Это достигается с помощью отсортированных наборов Redis .

2. rollwindow.py:

Если вы знаете какой-нибудь лучший способ сделать это с Redis — пожалуйста, дайте мне знать — я здесь не эксперт. Это первая реализация, которую я понял.

"""

    Redis rolling time window counter and rate limit.

    Use Redis sorted sets to do a rolling time window counters and limiters.

    http://redis.io/commands/zadd

"""

import time


def check(redis, key, window=60, limit=50):
    """ Do a rolling time window counter hit.

    :param redis: Redis client

    :param key: Redis key name we use to keep counter

    :param window: Rolling time window in seconds

    :param limit: Allowed operations per time window

    :return: True is the maximum limit has been reached for the current time window
    """

    # Expire old keys (hits)
    expires = time.time() - window
    redis.zremrangebyscore(key, '-inf', expires)

    # Add a hit on the very moment
    now = time.time()
    redis.zadd(key, now, now)

    # If we currently have more keys than limit,
    # then limit the action
    if redis.zcard(key) > limit:
        return True

    return False


def get(redis, key):
    """ Get the current hits per rolling time window.

    :param redis: Redis client

    :param key: Redis key name we use to keep counter

    :return: int, how many hits we have within the current rolling time window
    """
    return redis.zcard(key)

3. Проблемные капчи

Каждый из нас ненавидит капчу . Это обоюдоострые мечи. С одной стороны, вам нужно держать ботов на своем сайте. С другой стороны, CAPTCHA отключены для посетителей вашего сайта, и они отгоняют потенциальных пользователей.

Несмотря на то, что самая популярная CAPTCHA как услуга, reCAPTCHA от Google, добилась значительных успехов в создании CAPTCHA для реальных посетителей и для ботов , CAPTCHA по-прежнему представляют проблему с удобством использования. Также в случае с reCAPTCHA JavaScript и графические ресурсы загружаются из внешних сервисов Google, и они, как правило, блокируются в Китае, отключая ваш сайт для посетителей из Китая .

4. CAPTCHA и разные ситуации входа

Есть три случая, когда вы хотите, чтобы пользователь заполнил CAPTCHA для входа

  • Кто-то брутфорсит одно имя пользователя (целевая атака): вам нужно считать логины по имени пользователя и не позволять входить в систему, если этот пользователь получает слишком много логинов.
  • Кто-то просматривает комбинации имени пользователя и пароля для одного IP: вы учитываете логины на каждый IP.
  • Кто-то просматривает комбинации имени пользователя и пароля, и атака происходит с очень большого пула IP. Обычно это атаки на основе ботнетов, и злоумышленнику легко запрограммировать десятки тысяч IP-адресов.

Атака с помощью ботнета на вход в систему сложно блокировать. Может быть только одна попытка входа с каждого IP. Единственный способ эффективно остановить атаку — это представить CAPTCHA перед входом в систему, то есть пользователь должен решить CAPTCHA даже до того, как попытка входа будет предпринята. Однако предварительный вход в систему CAPTCHA очень раздражает в плане удобства использования — он не позволяет использовать менеджер паролей браузера для быстрого входа в систему и иногда дает дополнительную головную боль в течение двух минут, прежде чем вы перейдете на свой любимый сайт.

Даже такие сервисы, как CloudFlare , здесь вам не помогут. Поскольку существует только один запрос на один IP, они не могут заранее знать, будет ли запрос законным или нет (хотя у них наверняка есть глобальные эвристики и черные списки IP). Вы можете щелкнуть «вызов» на своем сайте, так что каждый посетитель должен заполнить CAPTCHA, прежде чем они смогут получить доступ к вашему сайту, и это удобство использования снова подводит.

5. Смягчение атаки входа в систему с помощью ботнета с помощью CAPTCHA

Вы можете иметь лучшее из обоих миров: не входить в систему CAPTCHA и по-прежнему смягчать атаку, вызванную ботнетом. Это может быть сделано

  • Отслеживание скорости входа на ваш сайт
  • В нормальной ситуации нет предварительной регистрации CAPTCHA
  • Когда явно присутствует ненормальная скорость входа в систему, что означает, что может происходить атака, включите CAPTCHA перед входом в систему на определенное время

Ниже приведен пример псевдо-Python, как этого можно достичь с помощью модуля Python RollingWindow из приведенного выше.

6. captchamode.py

from redis_cache import get_redis_connection

import rollingwindow


#: Redis sorted set key counting login attempts
REDIS_LOGIN_ATTEMPTS_COUNTER = "login_attempts"

#: Key telling that CAPTCHA become activated due to
#: high login attempts rate
REDIS_CAPTCHA_ACTIVATED = "captcha_activated"

#: Captcha mode expires in 120 minutes (attack cooldown)
CAPTCHA_TIMEOUT = 120 * 60

#: Are you presented CAPTCHA when logging in first time
#: Disabled in unit tests.
LOGIN_ATTEMPTS_CHALLENGE_THRESHOLD = 500  # per minute


def clear():
    """ Resets the challenge system state, per system or per IP. """
    redis = get_redis_connection("redis")
    redis.delete(REDIS_CAPTCHA_ACTIVATED)
    redis.delete(REDIS_LOGIN_ATTEMPTS_COUNTER)


def get_login_rate():
    """
    :return: System global login rate per minute for metrics
    """
    redis = get_redis_connection("redis")
    return rollingwindow.get(redis, REDIS_LOGIN_ATTEMPTS_COUNTER)


def check_captcha_needed(redis):
    """ Check if we need to enable login CAPTCHA globally.

    Increase login page load/submit counter.

    :return: True if our threshold for login page loads per minute is exceeded
    """

    # Count a hit towards login rate
    threshold_exceeded = rollingwindow.check(redis, REDIS_LOGIN_ATTEMPTS_COUNTER, limit=LOGIN_ATTEMPTS_CHALLENGE_THRESHOLD)

    # Are we in attack mode
    if not redis.get(REDIS_CAPTCHA_ACTIVATED):

        if not threshold_exceeded:
            # No login rate threshold exceeded,
            # and currently CAPTCHA not activated ->
            # allow login without CAPTCHA
            return False

        # Login attempt threshold exceeded,
        # we might be under attack,
        # activate CAPTCHA mode
        redis.setex(REDIS_CAPTCHA_ACTIVATED, "true", CAPTCHA_TIMEOUT)

    return True


def login(request):

    redis = get_redis_connection("redis")

    if check_captcha_needed(request):
        # ... We need to CAPTCHA before this login can proceed ..
    else:
        # ... Allow login to proceed without CAPTCHA ...