如何限制PHP中的用户登录尝试

时间:2010-01-19 03:42:08

标签: security throttling honeypot

我刚刚阅读了这篇关于防止快速登录尝试的帖子The definitive guide to form-based website authentication

最佳做法#1:短时间延迟会随着尝试失败次数而增加,例如:

1次失败尝试=没有延迟
2次失败尝试= 2秒延迟
3次失败尝试= 4秒延迟
4次失败尝试= 8秒延迟
5次失败尝试= 16秒延迟
等。

DoS攻击这个方案是非常不切实际的,但另一方面,可能具有破坏性,因为延迟会呈指数增长。

我很好奇我如何在PHP中为我的登录系统实现这样的东西?

12 个答案:

答案 0 :(得分:75)

您不能通过将限制链接到单个IP或用户名来简单地阻止DoS攻击。好吧,你甚至无法阻止使用这种方法的快速登录尝试。

为什么? 因为攻击可以跨越多个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表中提取的统计数据,因为它将根据用户数量随时更改以及有多少人可以调用(和键入)他们的密码。< / p>


> 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可以确保停止来自多个前端的攻击,并且正常站点用户不会遇到合法失败登录尝试的重大延迟。

答案 1 :(得分:5)

您有三种基本方法:存储会话信息,存储cookie信息或存储IP信息。

如果您使用会话信息,最终用户(攻击者)可以强制调用新会话,绕过您的策略,然后再次登录,不会有任何延迟。会话非常简单,只需将用户的最后已知登录时间存储在会话变量中,将其与当前时间匹配,并确保延迟足够长。

如果你使用cookies,攻击者可以简单地拒绝cookie,总而言之,这真的不可行。

如果您跟踪IP地址,则需要以某种方式存储来自IP地址的登录尝试,最好是在数据库中。当用户尝试登录时,只需更新您录制的IP列表即可。您应该以合理的间隔清除此表,转储在一段时间内未处于活动状态的IP地址。陷阱(总是存在缺陷)是,某些用户可能最终共享IP地址,并且在边界条件下,您的延迟可能会无意中影响用户。由于您正在跟踪登录失败,并且只登录失败,因此不会造成太大的痛苦。

答案 2 :(得分:4)

登录过程需要降低成功和失败登录的速度。登录尝试本身永远不会超过大约1秒。如果是,暴力使用延迟来知道尝试失败,因为成功比失败短。然后,可以每秒评估更多组合。

每台计算机的同时登录尝试次数需要由负载均衡器限制。最后,您只需要跟踪多个用户/密码登录尝试是否重用相同的用户或密码。人类的打字速度不超过每分钟约200字。因此,连续或同时登录尝试的速度超过每分钟200字,来自一组机器。因此,这些可以安全地通过管道传输到黑名单,因为它不是您的客户。每个主机的黑名单时间不需要大于约1秒。这绝不会给人类带来不便,但无论是串行还是并行,都会对蛮力企图造成严重破坏。

每秒一个组合的2 * 10 ^ 19个组合,在40亿个独立的IP地址上并行运行,将耗费158年作为搜索空间。为了每个用户持续一天对抗40亿攻击者,您需要一个完全随机的字母数字密码,至少9个位置。考虑用密码短语训练用户至少13个地方,1.7 * 10 ^ 20组合。

此延迟会激励攻击者窃取您的密码哈希文件,而不是强行破坏您的网站。使用经过批准,命名的散列技术。禁止整个互联网IP的人口一秒钟,将限制并行攻击的影响,而人类不会感激。最后,如果您的系统在一秒内允许超过1000次失败的登录尝试而没有对禁止系统做出某些响应,那么您的安全计划就会遇到更大的问题。首先修复自动响应。

答案 3 :(得分: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 :(得分:3)

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

答案 5 :(得分:2)

您可以使用会话。只要用户登录失败,您就会增加存储尝试次数的值。您可以根据尝试次数计算所需的延迟,也可以设置允许用户在会话中再次尝试的实际时间。

更可靠的方法是在数据库中为特定的ipaddress存储尝试和新尝试时间。

答案 6 :(得分:2)

恕我直言,防御DOS攻击最好在网络服务器级别(或者甚至在网络硬件中)处理,而不是在PHP代码中处理。

答案 7 :(得分:1)

我通常会创建登录历史记录和登录尝试表。尝试表将记录用户名,密码,IP地址等。查询表以查看是否需要延迟。我建议在给定时间(例如一小时)内完全阻止大于20的尝试。

答案 8 :(得分:1)

根据上面的讨论,会话,cookie和IP地址无效 - 所有这些都可以被攻击者操纵。

如果您想要防止暴力攻击,那么唯一可行的解​​决方案是根据提供的用户名进行尝试次数,但请注意,这允许攻击者通过阻止有效用户登录来阻止该站点。

e.g。

$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

下进行。

答案 9 :(得分:1)

在这种情况下,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指令。因此,每个请求获得相同的$ time和$ attempts值并执行。对于复杂的应用程序来说,防止这种安全问题可能很困难,并且涉及锁定和解锁数据库的某些表/行,当然会减慢应用程序的速度。

答案 10 :(得分:1)

简短的回答是:不要这样做。你不会保护自己免受暴力强迫,你甚至可以使你的情况变得更糟。

所提议的解决方案都不起作用。如果您使用IP作为限制的任何参数,攻击者将跨越大量IP进行攻击。如果您使用会话(cookie),攻击者将丢弃任何cookie。所有你能想到的总和是,绝对没有任何暴力迫使攻击者无法克服。

但有一件事 - 您只需依赖尝试登录的用户名。因此,不要查看所有其他参数,而是跟踪用户尝试登录和限制的频率。但是攻击者想要伤害你。如果他意识到这一点,他也会蛮力用户名。

这会导致几乎所有用户在尝试登录时都被限制为最大值。您的网站将毫无用处。攻击者:成功。

您可以将密码检查延迟大约200毫秒 - 网站用户几乎不会注意到这一点。但是一个野蛮的人会。 (同样,他可以跨越IP)但是,这一切并不能保护你免受暴力破坏或DDoS的侵害 - 因为你无法以编程方式进行。

执行此操作的唯一方法是使用基础架构。

您应该使用bcrypt而不是MD5或SHA-x来对您的密码进行哈希处理,如果有人窃取您的数据库,这将使您的密码更难解密(因为我猜您是在共享或托管主机上)

很抱歉让你失望,但是这里的所有解决方案都有一个弱点,没有办法在后端逻辑中克服它们。

答案 11 :(得分:0)

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