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

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

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

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

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

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

12 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

По сути, начальный код будет:

$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!";
}

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

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

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

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

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

например

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

НТН

C.

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

Файлы 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);
}

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

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;
        }
     }        
  }
}
Другие вопросы по тегам