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


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

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

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

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

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

12 51

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

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

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

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

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

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

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

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

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

или как предложил 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!";
}

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

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

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

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

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

HTH

C.

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

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

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

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

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

вы можете задержать проверку пароля в целом около 200 мс - пользователь сайта почти не заметит этого. Но грубый форсер будет. (Опять же он может охватывать IPs) однако ничто из всего этого не защитит вас от грубого принуждения или 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;
        }
     }        
  }
}