如何限制用户在PHP中的login尝试

我只是读这篇文章基于forms的网站身份validation防止快速login尝试的权威指南 。

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

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

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

我很好奇我怎么能在PHP中为我的login系统实现这样的东西?

您不能简单地通过将节stream链接到单个IP或用户名来防止DoS攻击。 地狱,你甚至不能真正防止使用这种方法的快速login尝试。

为什么? 由于攻击可以跨越多个IP和用户帐户,以绕过您的限制尝试。

我在其他地方看到过,理想情况下,您应该跟踪整个网站上所有失败的login尝试,并将它们关联到一个时间戳:

 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; 

根据给定时间内失败login的次数(本例中为15分钟)确定某些延迟阈值。 您应该根据从您的failed_logins表中提取的统计数据,因为它会根据用户数量以及他们可以调用(和键入)其密码的数量随时间变化


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

在每次失败的login尝试中查询表格,以查找给定时间段(例如15分钟)的失败login次数:


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

如果在给定时间段内的尝试次数超过了您的限制,则强制执行限制或强制所有用户使用validation码(即,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在一定的阈值将确保多方面的攻击将被停止,正常的站点用户不会经历合法的失败login尝试显着的延迟。

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

如果你使用会话信息,最终用户(攻击者)可以强行调用新的会话,绕过你的策略,然后再次login,没有任何延迟。 会话实现相当简单,只需将用户最后一次已知的login时间存储在会话variables中,将其与当前时间相匹配,并确保延迟时间足够长。

如果您使用cookies,攻击者可以简单地拒绝cookie,总而言之,这实际上并不可行。

如果您跟踪IP地址,则需要以某种方式存储来自IP地址的login尝试,最好是存储在数据库中。 当用户尝试login时,只需更新您logging的IP列表。 您应该以合理的时间间隔清除此表,并转储一段时间内未激活的IP地址。 陷阱(总是有一个陷阱)是,有些用户最终可能会共享IP地址,而在边界条件下,您的延迟可能会影响用户。 由于您正在跟踪login失败,只有login失败,这不应该导致太多的痛苦。

 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的build议:

 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在数据库中存储失败尝试。 (既然你有一个login系统,我假设你知道如何做到这一点。)

很显然,会话是一种诱人的方法,但真正专注的人可以很容易地意识到,他们可以简单地在失败的尝试中删除会话cookie,以完全避开节stream。

尝试login时,获取最近的(最后15分钟)login尝试次数以及最近一次尝试的时间。

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

login过程需要降低成功和失败login的速度。 login尝试本身不应该比大约1秒钟更快。 如果是,蛮力使用延迟知道尝试失败,因为成功比失败短。 然后,可以每秒评估更多的组合。

每台机器的同时login尝试次数需要由负载均衡器来限制。 最后,只需要跟踪多个用户/密码login尝试是否重复使用相同的用户或密码。 人类不能input每个微小约200字以上的字。 因此,连续或同时login的尝试速度超过每个微小200字是来自一组机器。 因此,这些可以安全地连接到黑名单,因为它不是您的客户。 每个主机的黑名单时间不需要大于1秒。 这绝对不会给人类带来不便,但是无论是串联还是并联,都可能造成混乱。

每秒一个组合的2 * 10 ^ 19个组合,在40亿个独立的IP地址上并行运行,将花费158年的时间作为search空间。 对于每个用户持续一天的攻击者数量达到40亿,您至less需要9个长度的随机字母数字密码。 考虑用通行短语至less13位长,1.7 * 10 ^ 20的组合来训练用户。

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

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

更可靠的方法是将尝试和新尝试时间存储在该特定IP地址的数据库中。

我通常创buildlogin历史logging和login尝试表。 尝试表将logging用户名,密码,IP地址等查询表,看看是否需要延迟。 我build议在给定的时间内(例如一个小时)完全阻止超过20次的尝试。

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

如果你想防止蛮力攻击,那么唯一可行的解​​决办法是以提供的用户名为基础进行尝试次数,但是请注意,这使得攻击者可以通过阻止有效用户login来对站点进行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 } 

请注意,正如所写的,这个过程泄露了安全信息 – 也就是说攻击系统的人有可能看到用户login的时间(攻击者的响应时间将下降到0)。 您也可以调整algorithm,以便根据之前的延迟和文件上的时间戳来计算延迟。

HTH

C。

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

在这种情况下,Cookie或基于会话的方法当然是无用的。 应用程序必须检查以前的login尝试的IP地址或时间戳(或两者)。

如果攻击者拥有多个IP来启动他/她的请求,则可以绕过IP检查,如果多个用户使用同一IP连接到您的服务器,则可能会造成麻烦。 在后一种情况下,多次login失败的用户将阻止共享相同IP的所有人在一段时间内以该用户名login。

时间戳检查与上面的问题相同:每个人都可以通过多次尝试来阻止其他人login特定的帐户。 使用validation码而不是长时间等待最后一次尝试可能是一个很好的解决方法。

login系统应该防止的唯一的额外事情是在尝试检查function上的竞态条件。 例如,在下面的伪代码中

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

如果攻击者同时向login页面发送请求会发生什么情况? 可能所有的请求都会以相同的优先级运行,并且有机会在没有请求到达increment_attempt_number指令之前,其他的请求就会到达第二行。 所以每个请求获得相同的$时间和$企图值,并执行。 防止这种安全问题对于复杂的应用程序来说可能是困难的,并且涉及locking和解锁数据库的某些表/行,当然也会减慢应用程序的速度。

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

所提出的解决scheme都不会起作用。 如果使用IP作为限制参数,攻击者将跨越大量的IP跨越攻击。 如果您使用会话(cookie),攻击者将只删除任何cookie。 所有你能想到的总和,绝对没有什么强制攻击者无法克服的。

但是有一件事 – 只要依靠试图login的用户名就可以了。所以,不要看所有其他参数,而是跟踪用户试图login和加油的频率。 但是攻击者想要伤害你。 如果他认识到这一点,他也会蛮横用户名。

这将导致几乎所有用户在尝试login时被扼杀到最大值。您的网站将毫无用处。 攻击者:成功

一般情况下,您可以延迟密码检查200ms左右 – 网站用户几乎不会注意到这一点。 但是一个蛮横的将会。 (他又可以跨越IP)然而,这一切都不会保护你免受暴力强迫或DDoS攻击,因为你无法编程。

唯一的方法就是使用基础设施。

您应该使用bcrypt而不是MD5或SHA-x来散列您的密码,这将使您的密码更加难以解密,如果有人窃取您的数据库(因为我猜您在共享或托pipe主机上)

对不起,令你失望的是,但是这里的所有解决scheme都有一个弱点,在后端逻辑中没有办法克服它们。

cballuo提供了一个很好的答案。 我只是想通过提供一个支持mysqli的更新版本来返回这个优势。 我稍微改变了sqls和其他小东西中的table / field列,但它应该可以帮助任何寻找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; } } } }