Как я могу сужать попытки входа пользователя в PHP?

Я просто читал это сообщение Окончательное руководство по проверке подлинности веб-сайта на основе форм о предотвращении попыток входа в систему быстрого доступа.

Лучшая практика № 1: короткая временная задержка, которая увеличивается с количеством неудачных попыток, например:

1 неудачная попытка = без задержки
2 неудачных попытки = 2 с задержка
3 неудачных попытки = 4 с с задержкой
4 неудачных попытки = задержка 8 секунд
5 неудачных попыток = задержка 16 секунд
и т.д.

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

Мне интересно, как я мог бы реализовать что-то подобное для моей системы входа в PHP?

Ответ 1

Вы не можете просто предотвращать атаки DoS, цепляя дросселирование до одного IP-адреса или имени пользователя. Черт, вы не можете даже реально предотвратить быстрые попытки входа в систему с использованием этого метода.

Почему? Поскольку атака может охватывать несколько IP-адресов и учетных записей пользователей, чтобы обойти ваши попытки дросселирования.

Я видел в другом месте, что в идеале вы должны отслеживать все неудачные попытки входа на сайт и связывать их с меткой времени, возможно:

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

Быстрая заметка в поле ip_address: вы можете хранить данные и извлекать данные, соответственно, с помощью INET_ATON() и INET_NTOA(), которые по существу эквивалентны преобразованию ip-адреса в и без целого числа без знака.

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

Определите определенные пороговые значения задержки в зависимости от общего количества неудачных логинов за определенный промежуток времени (15 минут в этом примере). Вы должны основывать это на статистических данных, извлеченных из вашей таблицы failed_logins, поскольку она будет меняться с течением времени в зависимости от количества пользователей и сколько из них может вызвать (и ввести) свой пароль.


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

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


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

Если количество попыток за данный период времени превышает ваш лимит, либо принудительно принудительно регулировать, либо заставить всех пользователей использовать капчу (т.е. reCaptcha), пока количество неудачных попыток за данный период времени не будет меньше порога,

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) {
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) {
            if ($failed_attempts > $attempts) {
                // we need to throttle based on delay
                if (is_numeric($delay)) {
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                } else {
                    // code to display recaptcha on login form goes here
                }
                break;
            }
        }        
    }
}

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

Ответ 2

У вас есть три основных подхода: хранить информацию о сеансе, хранить информацию о файлах cookie или хранить информацию о IP-адресе.

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

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

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

Ответ 3

session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

или, как было предложено Cyro:

sleep(2 ^ (intval($_SESSION['hit']) - 1));

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

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

В основном, начальный код:

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()
{
    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    {
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    }
    else
    {
        return 0;
    }
}

Ответ 4

Хранить неудачные попытки в базе данных по IP. (Поскольку у вас есть система входа в систему, я предполагаю, что вы хорошо знаете, как это сделать.)

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

При попытке войти в систему, выберите, сколько последних (скажем, последних 15 минут) попыток входа в систему было и время последней попытки.

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) {
    echo "Wait $remaining_delay more seconds, silly!";
}

Ответ 5

Процесс регистрации требует снижения его скорости как для успешного, так и для неудачного входа. Сама попытка входа никогда не должна быть быстрее, чем около 1 секунды. Если это так, грубая сила использует задержку, чтобы знать, что попытка не удалась, потому что успех короче неудачи. Затем можно оценивать больше комбинаций в секунду.

Количество одновременных попыток входа на машину должно быть ограничено балансировщиком нагрузки. Наконец, вам просто нужно отслеживать, если один и тот же пользователь или пароль повторно используется несколькими попытками входа в систему пользователя/пароля. Люди не могут набирать быстрее, чем около 200 слов на минет. Таким образом, последовательные или одновременные попытки входа в систему быстрее, чем 200 слов на миниатюры, - это набор машин. Таким образом, они могут быть отправлены в черный список безопасно, так как это не ваш клиент. Чёрный список раз на хост не должен превышать 1 секунду. Это никогда не причинит человеку неудобства, но будет иметь хаос с грубой силой, будь то в серийном или параллельном режиме.

2 * 10 ^ 19 комбинаций по одной комбинации в секунду, параллельной на 4 миллиарда отдельных IP-адресов, потребуется 158 лет, чтобы выхлопать в качестве пространства поиска. Чтобы продлиться один день на одного пользователя против 4-миллиардных злоумышленников, вам понадобится полностью случайный буквенно-цифровой пароль длиной 9 баллов. Рассмотрите возможность обучения пользователей в фразах прохода не менее 13 мест, 1.7 * 10 ^ 20 комбинаций.

Эта задержка заставит злоумышленника украсть ваш хэш файл паролей, а не грубо удалять ваш сайт. Используйте одобренные, названные, хэширующие методы. Запрещение всего населения интернет-IP в течение одной секунды ограничит эффект параллельных атак без особого, что человек оценит. Наконец, если ваша система разрешает более 1000 неудачных попыток входа в систему за одну секунду без какого-либо ответа на системы запрета, тогда ваши планы безопасности будут иметь больше проблем для работы. Исправьте этот автоматический ответ в первую очередь.

Ответ 6

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

Более надежным методом было бы сохранение попыток и нового времени пробы в базе данных для этого конкретного ipaddress.

Ответ 7

Я обычно создаю таблицы регистрации и таблицы попыток входа в систему. Таблица попыток будет записывать имя пользователя, пароль, IP-адрес и т.д. Запрос к таблице, чтобы узнать, нужно ли задерживать. Я бы рекомендовал полностью блокировать попытки, превышающие 20 за данный момент (например, час).

Ответ 8

Как обсуждалось выше, сеансы, файлы cookie и IP-адреса не эффективны - все может быть атаковано злоумышленником.

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

например.

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

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

НТН

С.

Ответ 9

IMHO, защита от DOS-атак лучше решать на уровне веб-сервера (или, может быть, даже в сетевом оборудовании), а не в вашем PHP-коде.

Ответ 10

В этом случае куки файлы или сеансовые методы бесполезны. Приложение должно проверять IP-адрес или временные метки (или оба) предыдущих попыток входа.

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

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

Единственными дополнительными вещами, которые должна предотвратить система входа, являются условия гонки в функции проверки попыток. Например, в следующем псевдокоде

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

Что произойдет, если злоумышленник отправляет одновременные запросы на страницу входа? Вероятно, все запросы будут работать с одинаковым приоритетом, и, скорее всего, ни один запрос не получает инструкции increment_attempt_number, пока другие не пройдут вторую строку. Таким образом, каждый запрос получает одинаковое значение $time и $stops и выполняется. Предотвращение подобных проблем безопасности может быть сложным для сложных приложений и связано с блокировкой и разблокировкой некоторых таблиц/строк базы данных, что, естественно, замедляет работу приложения.

Ответ 11

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

Ни одно из предлагаемых решений не будет работать. Если вы используете IP как любой параметр для дросселирования, злоумышленник просто охватит атаку через огромное количество IP-адресов. Если вы используете сеанс (cookie), злоумышленник просто удалит файлы cookie. Сумма всего, о чем вы можете думать, заключается в том, что абсолютно ничего не может сделать жестокий форсирующий атакующий.

Однако есть одна вещь: вы просто полагаетесь на имя пользователя, пытающееся войти в систему. Таким образом, не глядя на все остальные параметры, вы отслеживаете, как часто пользователь пытался войти в систему и отключить дроссель. Но злоумышленник хочет навредить вам. Если он это признает, он просто будет использовать имена пользователей грубой силы.

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

Вы можете отложить проверку пароля в целом за 200 мс - пользователь сайта почти не заметит этого. Но скот-форкер будет. (Опять же, он мог охватывать IP-адреса). Однако ничего из этого не будет защищать вас от жестокого форсирования или DDoS - так как вы не можете программно.

Единственный способ сделать это - использовать инфраструктуру.

Вы должны использовать bcrypt вместо MD5 или SHA-x для хэширования ваших паролей, это сделает дешифрование ваших паролей более сложным, если кто-то украдет вашу базу данных (потому что я думаю, что вы находитесь на общем или управляемом узле)

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

Ответ 12

cballuo дал отличный ответ. Я просто хотел вернуть пользу, предоставив обновленную версию, поддерживающую mysqli. Я немного изменил столбцы таблицы/поля в sqls и других мелочах, но это должно помочь любому, кто ищет эквивалент mysqli.

function get_multiple_rows($result) {
  $rows = array();
  while($row = $result->fetch_assoc()) {
    $rows[] = $row;
  }
  return $rows;
}

$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) {

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) {

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) {
        if ($failed_attempts > $attempts) {
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) {
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                }                

            break;
        }
     }        
  }
}