Как я могу ограничить попытки входа пользователя в PHP

StackOverflow https://stackoverflow.com/questions/2090910

  •  21-09-2019
  •  | 
  •  

Вопрос

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

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

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

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

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

Это было полезно?

Решение

Вы не можете просто предотвратить 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 на определенном пороге гарантирует, что атака с нескольких фронтов будет остановлена, и обычные пользователи сайта не столкнутся со значительной задержкой из-за законных неудачных попыток входа в систему.

Другие советы

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

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

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

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

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

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

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

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

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.

или как предложил Сайро:

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;
    }
}

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

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

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

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 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!";
}

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

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

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

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

Как указано выше, сеансы, файлы 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).Вы также можете настроить алгоритм так, чтобы задержка рассчитывалась на основе предыдущей задержки и метки времени в файле.

ХТХ

С.

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

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

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

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

$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);
}

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

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

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

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

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

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

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

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

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

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;
        }
     }        
  }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top