s5
职业顶帖手
级别: 博士生
UID: 32
精华: 0
发帖: 103
威望: 121 点
积分转换
愚愚币: 37 YYB
在线充值
贡献值: 0 点
在线时间: 182(小时)
注册时间: 2006-06-15
最后登录: 2010-01-31
楼主  发表于: 2006-06-15 20:09

 验证与授权

管理提醒: 本帖被 lou1984 从 文献检索知识 移动到本区(2010-04-08)
验证与授权
很多Web应用被其糟糕的身份验证与授权机制所困扰。本章主要讨论相关这些机制的漏洞,传授一些帮助你不犯通病的方法。我将通过一些例子进一步说明这些方法,但请注意不要把这些示例与其上下文割裂开来看,理解其中包含的原则和方法是很重要的。只有到那个时候你才能对它们进行正确运用。

通过验证我们可以确定一个用户的身份。典型的做法是简单地使用用户名和密*****进行检查。这样我们就能确定登录用户是一个授权用户。

身份验证,常被称为访问控制,是一种你能用来保护对受限资源的访问及确认一个用户是否有权访问特定资源的方法。例如,很多WEB应用会有只对授权用户开放的资源、只对系统管理员开放的资源以及对所有用户开放的资源。

产生访问控制漏洞的一个主要原因是粗心大意——在Web应用中这一部分程序是最少被关心和注意的。在开发中管理功能和访问控制常常是最后考虑的,而且一般是从授权用户的出发点去考虑和编写,并没有考虑攻击者可能会使用的手段。对授权用户的信任远远高过匿名用户,但是如果你的管理功能是能通过URL直接公开访问的话,就会成为攻击者钟爱的目标。疏忽是你首要的大敌。

为达到安全的目的,需要在设计时就综合考虑访问控制。它不应只是一个现有应用的门闩,尽管有时可能是这样的,但该流程是非常容易发生错误的,同时访问控制中的错误必然导致安全漏洞。

访问控制还需要一个可靠的识别机制。毕竟,如果一个攻击者能伪装成一个合法用户,所有的基于用户识别的访问控制是无效的。因此,你要考虑到攻击的情况,例如会话劫持。关于会话相关的攻击手段请参见第四章。

本章涉及相关验证与授权中通常需要**************的四个方面:暴力攻击,密*****嗅探,重播攻击以及永久登录。

7.1. 暴力攻击
暴力攻击是一种不使用任何特殊手段而去穷尽各种可能性的攻击方式。它的更正式的叫法是穷举攻击——穷举各种可能性的攻击。

对于访问控制,典型的暴力攻击表现为攻击者通过大量的尝试去试图登录系统。在多数情况下,用户名是已知的,而只需要猜测密*****。

尽管暴力攻击没有技巧性可言,但词典攻击似乎有一定的技巧性。最大的区别是在进行猜测时的智能化。词典攻击只会最可能的情况列表中进行穷举,而不像暴力攻击一样去穷举所有的可能情况。

防止进行验证尝试或限制允许错误的次数还算是一个比较有效的安全手段,但是这样做的两难之处在于如何在不影响合法用户使用的情况下识别与阻止攻击者。

在这种情况下,对一致性的判定可以帮助你区分二者。这个方法与第四章中所述的防止会话劫持的做法很相似,但区别是你要确定的是一个攻击者而不是一个合法用户。

考虑下面的HTML表单:

CODE:

Username:


Password:






攻击者会察看这个表单并建立一段脚本来POST合法的数据给http://example.org/login.php

CODE:
$username = 'victim';
$password = 'guess';

$content = "username=$username&password=$password";
$content_length = strlen($content);

$http_request = '';
$http_response = '';

$http_request .= "POST /login.php HTTP/1.1\r\n";
$http_request .= "Host: example.org\r\n";
$http_request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$http_request .= "Content-Length: $content_length\r\n";
$http_request .= "Connection: close\r\n";
$http_request .= "\r\n";
$http_request .= $content;

if ($handle = fsockopen('example.org', 80))
{
fputs($handle, $http_request);

while (!feof($handle))
{
$http_response .= fgets($handle, 1024);
}

fclose($handle);

/* Check Response */
}
else
{
/* Error */
}

?>


使这段脚本,攻击者还可以简单地加入一个循环来继续尝试不同的密*****,并在每次尝试后检查$http_response变量。一旦$http_response变量有变化,就可以认为猜测到了正确的密*****。

你可以通过很多安全措施去防止此类攻击。我们注意到,在暴力攻击中每次的HTTP请求除了密*****是不同的,其他部分完全相同,这一点是很有价值的。

尽管在超过一定数量的失败尝试后临时冻结帐号是一种有效的防范手段,但你可能会去考虑采用更确定的方式去冻结帐号,以使攻击者更少地影响合法用户对你的应用的正常使用。

还有一些流程也可以增大暴力攻击的难度,使它不太可能成功。一个简单的遏制机制就能有效地做到这一点:

CODE:
/* mysql_connect() */
/* mysql_select_db() */

$clean = array();
$mysql = array();

$now = time();
$max = $now - 15;

$salt = 'SHIFLETT';

if (ctype_alnum($_POST['username']))
{
$clean['username'] = $_POST['username'];
}
else
{
/* ... */
}
$clean['password'] = md5($salt . md5($_POST['password'] . $salt));
$mysql['username'] = mysql_real_escape_string($clean['username']);

$sql = "SELECT last_failure, password
FROM users
WHERE username = '{$mysql['username']}'";

if ($result = mysql_query($sql))
{
if (mysql_num_rows($result))
{
$record = mysql_fetch_assoc($result);

if ($record['last_failure']> $max)
{
/* Less than 15 seconds since last failure */
}
elseif ($record['password'] == $clean['password'])
{
/* Successful Login */
}
else
{
/* Failed Login */

$sql = "UPDATE users
SET last_failure = '$now'
WHERE username = '{$mysql['username']}'";

mysql_query($sql);
}
}
else
{
/* Invalid Username */
}
}
else
{
/* Error */
}

?>


上例会限制在上次验证失败后对同一用户再试尝试的频率。如果在一次尝试失败后的15秒内再次尝试,不管密*****是否正确,验证都会失败。这就是这个方案的关键点。但简单地在一次失败尝试后15秒内阻止访问还是不够的——在此时不管输入是什么,输出也会是一致的,只有在登录成功后才会不同。否则,攻击者只要简单地检查不一致的输出即可确定登录是否成功。
7.2. 密*****嗅探
尽管攻击者通过嗅探(察看)你的用户和应用间的网络通信并不专门用于访问控制,但要意识到数据暴露变得越来越重要,特别是对于验证信息。

使用SSL可以有效地防止HTTP请求和回应不被暴露。对任何使用https方案的资源的请求可以防止密*****嗅探。最好的方法是一直使用SSL来发送验证信息,同时你可能还想用SSL来传送所有的包含会话标识的请求以防止会话劫持。

为防止用户验证信息不致暴露,在表单的action属性的URL中使用https方案如下:

CODE:

Username:


Password:






高度推荐在验证表单中使用POST方法,这是因为无论是否你使用了SSL,这样做与GET方法相比,验证信息较少暴露,。

尽管这样做只是为了保护用户的验证信息不被暴露,但你还是应该同时对HTML表单使用SSL。这样做不是出于技术上的原因,但是用户在看到表单被SSL所保护时,在输入验证信息时会感觉更为舒坦(见图7-1)。

图 7-1. 大多数浏览器在当前资源被SSL所保护时会显示一个锁形图标


7.3. 重播攻击
重播攻击,有时称为演示攻击,即攻击者重现以前合法用户向服务器所发送的数据以获取访问权或其它分配给该用户的权限。

与密*****嗅探一样,防止重播攻击也需要你意识到数据的暴露。为防止重播攻击,你需要加大攻击者获取任何用于取得受限资源的访问权限的数据的难度。这主要要求做到避免以下做法:

设定受保护资源永久访问权的数据的使用;
设定受保护资源访问权的数据的暴露(甚至是只提供临时访问权的数据);

这样,你应该只使用设定受保护资源临时访问权的数据,同时你还要尽力避免该数据泄露。这些虽只是一般的指导原则,但它们能为你的运作机制提供指导。

第一个原则据我所知,违反它的频率已达到了令人恐怖的程度。很多开发人员只注意保护敏感数据暴露,而忽视了用于设定受保护资源永久访问权的数据在使用时引发的风险。

例如,考虑一下用本地脚本计算验证表单密*****的hash值的情况。这样密*****的明文不会暴露,暴露的只是它的hash值。这就保护了用户的原始密*****。这个流程的主要问题是重播漏洞依然如故——攻击者可以简单的重播一次合法的验证过程即可通过验证,只要用户密*****是一致的,验证过程就会成功。

更安全的运行方案、MD5的JavaScript源文件以及其它算法,请看http://pajhome.org.uk/crypt/md5/

类似于对第一原则的违反是指定一个cookie以提供对某一资源的永久访问权。例如,请考虑下面的通过设定cookie运行的一个永久访问机制的尝试:

CODE:$auth = $username . md5($password);
setcookie('auth', $cookie);
?>


如果一个未验证用户提供了一个验证cookie,程序会检查在cookie中的密*****的hash值与存在数据库中的密*****的hash是否匹配。如果匹配,则用户验证通过。

本流程中的问题是该验证cookie的暴露是一个非常大的风险。如果它被捕获的话,攻击者就获得了永久访问权。尽管合法用户的cookie可能会过期,但攻击者可以每次都提供cookie以供验证。请看图7-2中对这种情形的图示。

一个更好的永久登录方案是只使用设定临时访问权的数据,这也是下一节的主题。
7.4. 永久登录
永久登录指的是在浏览器会话间进行持续验证的机制。换句话说,今天已登录的用户明天依然是处于登录状态,即使在多次访问之间的用户会话过期的情况下也是这样。

永久登录的存在降低了你的验证机制的安全性,但它增加了可用性。不是在用户每次访问时麻烦用户进行身份验证,而是提供了记住登录的选择。

图7-2. 攻击者通过重播用户的cookie进行未授权访问


据我观察,最常见的有缺陷的永久登录方案是将用户名和密*****保存在一个cookie中。这样做的诱惑是可以理解的——不需要提示用户输入用户名和密*****,你只要简单地从cookie中读取它们即可。验证过程的其它部分与正常登录完全相同,因此该方案是一个简单的方案。

不过如果你确实是把用户名和密*****存在cookie中的话,请立刻关闭该功能,同时阅读本节的余下内容以找到实现更安全的方案的一些思路。你将来还需要要求所有使用该cookie的用户修改密*****,因为他们的验证信息已经暴露了。

永久登录需要一个永久登录cookie,通常叫做验证cookie,这是由于cookie是被用来在多个会话间提供稳定数据的唯一标准机制。如果该cookie提供永久访问,它就会造成对你的应用的安全的严重风险,所以你需要确定你保存在cookie中的数据只能在有限的时间段内用于身份验证。

第一步是设计一个方法来减轻被捕获的永久登录cookie造成的风险。尽管cookie被捕获是你需要避免的,但有一个深度防范流程是最好的,特别是因为这种机制即使是在一切运行正常的情况下,也会降低验证表单的安全性。这样,该cookie就不能基于任何提供永久登录的信息来产生,如用户密*****。

为避免使用用户的密*****,可以建立一个只供一次验证有效的标识:

CODE:
$token = md5(uniqid(rand(), TRUE));

?>


你可以把它保存在用户的会话中以把它与特定的用户相关联,但这并不能帮助你在多个会话间保持登录,这是一个大前提。因此,你必须使用一个不同的方法把这个标识与特定的用户关联起来。

由于用户名与密*****相比要不敏感一些,你可以把它存在cookie中,这可以帮助验证程序确认提供的是哪个用户的标识。可是,一个更好的方法是使用一个不易猜测与发现的第二身份标识。考虑在保存用户名和密*****的数据表中加入三个字段:第二身份标识(identifier),永久登录标识(token),以及一个永久登录超时时间(timeout)。

mysql> DESCRIBE users;
+------------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+-------+
| username | varchar(25) | | PRI | | |
| password | varchar(32) | YES | | NULL | |
| identifier | varchar(32) | YES | MUL | NULL | |
| token | varchar(32) | YES | | NULL | |
| timeout | int(10) unsigned | YES | | NULL | |
+------------+------------------+------+-----+---------+-------+

通过产生并保存一个第二身份标识与永久登录标识,你就可以建立一个不包含任何用户验证信息的cookie。

CODE:
$salt = 'SHIFLETT';

$identifier = md5($salt . md5($username . $salt));
$token = md5(uniqid(rand(), TRUE));
$timeout = time() + 60 * 60 * 24 * 7;

setcookie('auth', "$identifier:$token", $timeout);

?>


当一个用户使用了一个永久登录cookie的情况下,你可以通过是否符合几个标准来检查:

CODE:
/* mysql_connect() */
/* mysql_select_db() */

$clean = array();
$mysql = array();

$now = time();
$salt = 'SHIFLETT';

list($identifier, $token) = explode(':', $_COOKIE['auth']);

if (ctype_alnum($identifier) && ctype_alnum($token))
{
$clean['identifier'] = $identifier;
$clean['token'] = $token;
}
else
{
/* ... */
}

$mysql['identifier'] = mysql_real_escape_string($clean['identifier']);

$sql = "SELECT username, token, timeout
FROM users
WHERE identifier = '{$mysql['identifier']}'";

if ($result = mysql_query($sql))
{
if (mysql_num_rows($result))
{
$record = mysql_fetch_assoc($result);

if ($clean['token'] != $record['token'])
{
/* Failed Login (wrong token) */
}
elseif ($now > $record['timeout'])
{
/* Failed Login (timeout) */
}
elseif ($clean['identifier'] !=
md5($salt . md5($record['username'] . $salt)))
{
/* Failed Login (invalid identifier) */
}
else
{
/* Successful Login */
}

}
else
{
/* Failed Login (invalid identifier) */
}
}
else
{
/* Error */
}

?>


你应该坚持从三个方面来限制永久登录cookie的使用。

l Cookie需在一周内(或更少)过期
l Cookie最好只能用于一次验证(在一次成功验证后即删除或重新生成)
l 在服务器端限定cookie在一周(或更少)时间内过期

如果你想要用户无限制的被记住,那只要是该用户的访问你的应用的频度比过期时间更大的话,简单地在每次验证后重新生成标识并设定一个新的cookie即可。

另一个有用的原则是在用户执行敏感操作前需要用户提供密*****。你只能让永久登录用户访问你的应用中不是特别敏感的功能。在执行一些敏感操作前让用户手工进行验证是不可替代的步骤。

最后,你需要确认登出系统的用户是确实登出了,这包括删除永久登录cookie:

CODE:
setcookie('auth', 'DELETED!', time());

?>


上例中,cookie被无用的值填充并设为立即过期。这样,即使是由于一个用户的时钟不准而导致cookie保持有效的话,也能保证他有效地退出。
分享:

愚愚学园属于纯学术、非经营性专业网站,无任何商业性质,大家出于学习和科研目的进行交流讨论。

如有涉侵犯著作权人的版权等信息,请及时来信告知,我们将立刻从网站上删除,并向所有持版权者致最深歉意,谢谢。