Статьи

Как встроить ограничение скорости в ваше веб-приложение

Конечный продукт
Что вы будете создавать

В то время как отчеты различаются, The Washington Post сообщила, что недавний взлом фотографий знаменитостей iCloud был сосредоточен вокруг незащищенной точки входа Find My iPhone:

«… исследователи в области безопасности, как говорили, нашли недостаток в функции« Найти мой iPhone »в iCloud, которая не пресекала атаки методом« грубой силы ». Заявление Apple … предполагает, что компания не рассматривает это открытие как проблему. проблема, по словам исследователя безопасности и сотрудника Washington Post Ашкана Солтани.

Я согласен. Я бы хотел, чтобы Apple была более откровенной; его тщательно сформулированный ответ оставлял место для различных толкований и, по-видимому, обвинял жертв.

Хакеры могли использовать этот скрипт iBrute на GitHub для таргетинга на аккаунты знаменитостей через Find My iPhone; с тех пор уязвимость была закрыта.

Поскольку одна из самых богатых корпораций в мире не выделяла ресурсы для ограничения скорости всех точек аутентификации, вероятно, некоторые из ваших веб-приложений не включают ограничение скорости. В этом руководстве я рассмотрю некоторые основные концепции ограничения скорости и простую реализацию для вашего веб-приложения на основе PHP.

Исследования предыдущих хаков выявили пароли, которые люди чаще всего используют. Xeno.net публикует список из десяти тысяч самых популярных паролей . На приведенной ниже диаграмме видно, что частота общих паролей в их списке 100 самых популярных составляет 40%, а число 500 лучших составляет 71%. Другими словами, люди обычно используют и повторно используют небольшое количество паролей; отчасти потому, что их легко запомнить и легко напечатать.

Частота общих паролей - от ксенонета

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

Как только хакер идентифицирует точку входа, которая позволяет неограниченное количество попыток входа в систему, он может автоматизировать высокоскоростные атаки с использованием большого объема словаря. Если нет ограничений по скорости, тогда хакерам будет легко атаковать с помощью больших и больших словарей — или автоматизированных алгоритмов с бесконечным числом перестановок.

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

Для защиты входов в систему в качестве основы я рекомендую несколько подходов:

  1. Ограничить количество неудачных попыток для определенного имени пользователя
  2. Ограничить количество неудачных попыток по IP-адресу

В обоих случаях мы хотим измерить неудачные попытки в течение определенного окна или промежутков времени, например, 15 минут и 24 часа.

Один риск блокирования попыток по имени пользователя состоит в том, что фактический пользователь может быть заблокирован из своей учетной записи. Поэтому мы хотим убедиться, что действительный пользователь может повторно открыть свою учетную запись и / или сбросить свой пароль.

Риск блокирования попыток по IP-адресу состоит в том, что они часто используются многими людьми. Например, в университете может быть и реальный владелец аккаунта, и кто-то, кто пытается злонамеренно взломать их аккаунт. Блокировка IP-адреса может блокировать как хакера, так и реального пользователя.

Тем не менее, одной из причин повышения безопасности часто является увеличение неудобств. Вы должны решить, как строго ограничивать тарифы на свои услуги и как легко вы хотите, чтобы пользователи могли заново открывать свои учетные записи.

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

Я написал немного кода, чтобы показать вам, как ограничить скорость ваших веб-приложений; мои примеры основаны на Yii Framework для PHP . Большая часть кода применима к любому приложению или среде PHP / MySQL.

Во-первых, нам нужно создать таблицу MySQL для хранения информации о неудачных попытках входа в систему. В таблице должны храниться ip_address запрашивающего пользователя, использованное имя пользователя или адрес электронной почты и временная метка:

1
2
3
4
5
6
$this->createTable($this->tableName, array(
   ‘id’ => ‘pk’,
   ‘ip_address’ => ‘string NOT NULL’,
   ‘username’ => ‘string NOT NULL’,
   ‘created_at’ => ‘TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP’,
   ), $this->MySqlOptions);

Затем мы создаем модель для таблицы LoginFail несколькими методами: добавление, проверка и очистка.

Всякий раз, когда происходит неудачный вход в систему, мы добавляем строку в таблицу LoginFail:

01
02
03
04
05
06
07
08
09
10
public function add($username) {
   // add a row to the failed login table with username and IP address
   $failure = new LoginFail;
   $failure->username = $username;
   $failure->ip_address = $this->getUserIP();
   $failure->created_at =new CDbExpression(‘NOW()’);
   $failure->save();
   // whenever there is a failed login, purge older failure log
   $this->purge();
 }

Для getUserIP() я использовал этот код из переполнения стека.

Мы также можем использовать возможность неудачного входа в систему, чтобы очистить таблицу от старых записей. Я делаю это, чтобы предотвратить замедление проверок во времени. Или вы можете реализовать операцию очистки в фоновой задаче cron каждый час или каждый день:

1
2
3
4
5
6
7
public function purge($mins=120) {
    // purge failed login entries older than $mins
    $minutes_ago = (time() — (60*$mins));
    $criteria=new CDbCriteria();
    LoginFail::model()->older_than($minutes_ago)->applyScopes($criteria);
    LoginFail::model()->deleteAll($criteria);
}

Модуль аутентификации Yii, который я использую, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public function authenticate($attribute,$params)
{
    if(!$this->hasErrors()) // we only want to authenticate when no input errors
    {
        $identity=new UserIdentity($this->username,$this->password);
        $identity->authenticate();
        if (LoginFail::model()->check($this->username)) {
            $this->addError(«username»,UserModule::t(«Account access is blocked, please contact support.»));
        } else {
        switch($identity->errorCode)
            {
                case UserIdentity::ERROR_NONE:
                        $duration=$this->rememberMe ?
                        Yii::app()->user->login($identity,$duration);
                    break;
                case UserIdentity::ERROR_EMAIL_INVALID:
                    $this->addError(«username»,UserModule::t(«Email is incorrect.»));
                    LoginFail::model()->add($this->username);
                    break;
                case UserIdentity::ERROR_USERNAME_INVALID:
                    $this->addError(«username»,UserModule::t(«Username is incorrect.»));
                    LoginFail::model()->add($this->username);
                    break;
                    case UserIdentity::ERROR_PASSWORD_INVALID:
                        $this->addError(«password»,UserModule::t(«Password is incorrect.»));
                        LoginFail::model()->add($this->username);
                        break;
                case UserIdentity::ERROR_STATUS_NOTACTIV:
                    $this->addError(«status»,UserModule::t(«You account is not activated.»));
                    break;
                case UserIdentity::ERROR_STATUS_BAN:
                    $this->addError(«status»,UserModule::t(«You account is blocked.»));
                    break;
            }
        }
    }
}

Всякий раз, когда мой код входа в систему обнаруживает ошибку, я вызываю метод, чтобы добавить подробности об этом в таблицу LoginFail:

LoginFail::model()->add($this->username);

Раздел проверки здесь. Это работает с каждой попыткой входа в систему:

1
2
3
$identity->authenticate();
if (LoginFail::model()->check($this->username)) {
    $this->addError(«username»,UserModule::t(«Account access is blocked, please contact support.»));

Вы можете перенести эти функции в раздел аутентификации входа в свой собственный код.

Моя проверка проверяет большой объем неудачных попыток входа в систему для рассматриваемого имени пользователя и отдельно для используемого IP-адреса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public function check($username) {
   // check if failed login threshold has been violated
   // for username in last 15 minutes and last hour
   // and for IP address in last 15 minutes and last hour
   $has_error = false;
   $minutes_ago = (time() — (60*15));
   $hours_ago = (time() — (60*60));
   $user_ip = $this->getUserIP();
   if (LoginFail::model()->since($minutes_ago)->username($username)->count()>=self::FAILS_USERNAME_QUARTER_HOUR) {
     $has_error = true;
   } else if (LoginFail::model()->since($minutes_ago)->ip_address($user_ip)->count()>=self::FAILS_IP_QUARTER_HOUR) {
       $has_error = true;
     } else if (LoginFail::model()->since($hours_ago)->username($username)->count()>=self::FAILS_USERNAME_HOUR) {
     $has_error = true;
   } else if (LoginFail::model()->since($hours_ago)->ip_address($user_ip)->count()>=self::FAILS_IP_HOUR) {
       $has_error = true;
     }
     if ($has_error)
         $this->add($username);
     return $has_error;
 }

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

1
2
3
4
const FAILS_USERNAME_HOUR = 6;
 const FAILS_USERNAME_QUARTER_HOUR = 3;
 const FAILS_IP_HOUR = 24;
 const FAILS_IP_QUARTER_HOUR = 12;

Обратите внимание, что мои проверки подтверждают использование именованных областей действия Yii ActiveRecord для упрощения кода запроса к базе данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// scope of rows since timestamp
 public function since($tstamp=0)
 {
   $this->getDbCriteria()->mergeWith( array(
     ‘condition’=>'(UNIX_TIMESTAMP(created_at)>’.$tstamp.’)’,
   ));
     return $this;
 }
 
 // scope of rows before timestamp
 public function older_than($tstamp=0)
 {
   $this->getDbCriteria()->mergeWith( array(
     ‘condition’=>'(UNIX_TIMESTAMP(created_at)<‘.$tstamp.’)’,
   ));
     return $this;
 }
 
 public function username($username=»)
 {
   $this->getDbCriteria()->mergeWith( array(
     ‘condition’=>'(username=»‘.$username.’»)’,
   ));
     return $this;
 }
 
 public function ip_address($ip_address=»)
 {
   $this->getDbCriteria()->mergeWith( array(
     ‘condition’=>'(ip_address=»‘.$ip_address.’»)’,
   ));
     return $this;
 }

Я попытался написать эти примеры, чтобы вы могли легко настроить их. Например, вы можете опустить чеки за последний час и рассчитывать на последние 15 минут. В качестве альтернативы вы можете изменить константы, чтобы установить более высокие или более низкие пороговые значения для количества входов в систему за интервал. Вы также можете написать гораздо более сложные алгоритмы. Тебе решать.

В этом примере для повышения производительности вы можете индексировать таблицу LoginFail по имени пользователя и отдельно по IP-адресу.

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

Надеюсь, вы нашли это интересным и полезным. Пожалуйста, не стесняйтесь размещать исправления, вопросы или комментарии ниже. Я был бы особенно заинтересован в альтернативных подходах. Вы также можете связаться со мной в Twitter @reifman или написать мне напрямую.

Кредиты: iBrute предварительный просмотр фото через Heise Security