忽略名字上的相似程度,伪造跨站请求(CSRF)是几乎完全相反的攻击方式。XSS 是利用用户对网站的信任展开攻击;CSRF 是利用网站对用户的信任展开攻击。CSRF 攻击更加危险,更少遇到(意味着对于开发者没有更多资料),并且比起 XSS 攻击更加难以防御。
CSRF 攻击发生在下面的情况下:
对于可获得网站信任的特定用户。
多数用户可能不被信任,但是 web 应用向用户提供特定的权限以便其登录进入应用程序是很普遍的。拥有很高的特权的用户往往都是受害者(事实上在自己不知道的情况下成为了同谋)。
通常网站信任用户的身份标识。用户的身份标识拥有着重要的地位。但是即便有安全的会话管理机制,CSRF 攻击仍然能够成功。而且事实上,对于这种情况 CSRF 攻击更加有效。
攻击者可随心所欲的执行 HTTP 请求。
在 CSRF 所有攻击方式中包含攻击者伪造一个看起来是其他用户发起的 HTTP 请求(事实上,跟踪一个用户发送的 HTTP 请求才是攻击者的目的)。有一部分技术可以用来完成这个,后面会演示一个使用特别技术的例子。
由于 CSRF 攻击包含伪造 HTTP 请求,熟悉底层 HTTP 协议就变得非常重要。
浏览器是 HTTP 客户端,而 web 服务器是 HTTP 服务器。客户端通过发送请求初始化一个传输,而服务器通过应答完成这个传输。一个标准的 HTTP 请求如下:
GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
第一行是请求行,包含请求的方式,请求的 URL(使用相对的 URL),和 HTTP 版本。其他行是 HTTP 头,每个头的名字后是一个冒号和一个空格,然后是值。
你可能熟悉使用 PHP 产生这些信息。例如,下面的代码可以用于构造这个原始的 HTTP 请求保存为字符串:
<?php
$request = '';
$request .= "{$_SERVER['REQUEST_METHOD']} ";
$request .= "{$_SERVER['REQUEST_URI']} ";
$request .= "{$_SERVER['SERVER_PROTOCOL']}\r\n";
$request .= "Host: {$_SERVER['HTTP_HOST']}\r\n";
$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\r\n";
$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}\r\n\r\n";
?>
响应前面的请求的应答如下:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 57
<html>
<img src="http://example.org/image.png" />
</html>
应答的内容就是你在浏览器中查看代码时看到的。这个应答中的 img 标签告诉浏览器取得另外一个资源(图象)并呈现在页面上。浏览器请求这个资源以及其他,下面这个例子关于这个请求:
GET /image.png HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
这里值得注意。浏览器请求在 img 标签的 src 属性指定的 URL,就如同用户手工定向到那里。浏览器无法明确指出请求的是一个图象。
将这个同之前了解的表单联系在一起,并且考虑一个如同下面的 URL :
http://stocks.example.org/buy.php?symbol=SCOX&quantity=1000
一个表单使用 GET 提交是无法同图象请求区分开的——两个都可以用相同的 URL 请求。如果 register_globals 开启,表单操作就不十分重要了(除非开发者使用 $_POST 以及相关)。危险似乎已经变得清晰了。
另外一个情况使得 CSRF 如此严重的原因是任何 URL 的 cookie 都是包含在对该 URL 的请求中。 一个准备同 stocks.example.org 建立联系的用户(比如已经登录)可以通过访问如同前面示例中的含有 img 标签的页面来购买 1000 份的 SCOX。 考虑下面这个表单(假想的):
http://stocks.example.org/form.html:
<p>立刻购买!</p>
<form action="/buy.php">
<p>代码:<input type="text" name="symbol" /></p>
<p>数量:<input type="text" name="quantity" /></p>
<input type="submit" />
</form>
如果用户输入 SCOX 作为代码,1000 作为数量,并且提交表单,浏览器发送的请求如下:
GET /buy.php?symbol=SCOX&quantity=1000 HTTP/1.1
Host: stocks.example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234
在本例中包含了 Cookie 头用于说明使用 cookie 作为 session 标识。如果一个 img 标签指向一同一个 URL,请求这个 URL 的时候相同的 cookie 也会被发送,服务器处理这个请求时无法区分是不是真正的订单。
有一些可以保护应用不受 CSRF 攻击的办法:
在表单中使用 POST 而不是 GET。表单的 method 属性中指定为 POST。当然,这并不适合所有的表单,但对于执行任务的表单来说是没有问题的,例如购买商品。事实上,HTTP 标准要求考虑到 GET 的安全。
使用 $_POST 而不是依赖 register_globals。如果信任 register_globals 并使用表单变量 $symbol 和 $quantity,那么 POST 方法对于防范 CSRF 攻击是没有什么作用的。同样使用 $_REQUEST 对于防范 CSRF 攻击也没有作用。
不要只留意易用性。
虽然考虑用户体验如易用性是很好的,但是过分的易用性可能引起严重的后果。虽然“只点一次”可以做得相当安全,但是简单的处理可能带来 CSRF 风险。
留意表单的用途。
CSRF 最大的问题是看起来是表单提交的数据实际上不是。如果用户没有请求带有表单的页面,是否能确定那个表单提交的数据是合法并可信的?
现在我们可以编写更加安全的留言版:
<?php
$token = md5(time());
$fp = fopen('./tokens.txt', 'a');
fwrite($fp, "$token\n");
fclose($fp);
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
$tokens = file('./tokens.txt');
if (in_array($_POST['token'], $tokens))
{
if (isset($_POST['message']))
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
readfile('./messages.txt');
?>
这个留言版仍然有一些安全问题。你能否发现它们?
使用时间极为容易预测。对时间戳进行 MD5 散列是很简陋的生成随机数码的办法。更好的方案包含 uniqid() 和 rand()。
更重要的是,攻击者很容易就可获得合法令牌(token)。只需要访问合法令牌产生并存储的文件,只要在请求中添加令牌,攻击像以前一样简单。
这里是改进的留言版:
<?php
session_start();
if (isset($_POST['message']) && isset($_SESSION['token']))
{
if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
readfile('./messages.txt');
?>