我刚阅读了这篇文章《网站认证的权威指南》,其中有关于防止快速登录尝试的内容。
最佳实践#1:一个短暂的时间延迟,随着失败次数的增加而逐渐增加,如下:
1次失败尝试=没有延迟
2次失败尝试=2秒延迟
3次失败尝试=4秒延迟
4次失败尝试=8秒延迟
5次失败尝试=16秒延迟
等等。
攻击此方案的DoS攻击是非常不现实的,但另一方面,这可能会是灾难性的,因为延迟呈指数增长。
我想知道如何在我的PHP登录系统中实现类似的功能?
我刚阅读了这篇文章《网站认证的权威指南》,其中有关于防止快速登录尝试的内容。
最佳实践#1:一个短暂的时间延迟,随着失败次数的增加而逐渐增加,如下:
1次失败尝试=没有延迟
2次失败尝试=2秒延迟
3次失败尝试=4秒延迟
4次失败尝试=8秒延迟
5次失败尝试=16秒延迟
等等。
攻击此方案的DoS攻击是非常不现实的,但另一方面,这可能会是灾难性的,因为延迟呈指数增长。
我想知道如何在我的PHP登录系统中实现类似的功能?
仅仅通过将限流应用于单个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地址转换为无符号整数,以及从无符号整数转换回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在一定阈值内可以确保多方面的攻击被停止,同时正常的站点用户在合法的登录失败尝试中不会经历显著的延迟。
failed_logins
表中需要id
列?假设您经常清空该表,但也许还有其他原因?顺便说一句,感谢您提供的出色答案。 - Kosta Kontos将失败的尝试按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!";
}
$remaining_delay = min(3600, $remaining_delay);
设为上限。 - Alix Axelsession_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;
}
}
在我看来,对抗DOS攻击最好是在Web服务器级别(甚至是在网络硬件中)处理,而不是在你的PHP代码中。
$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
}
在这种情况下,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);
}