一、概念:
CSRF:也叫XSRF(英文Cross-site request forgery),中文名是跨域请求伪造,也被称为One Click Attack或者Session Riding。CSRF是指恶意用户伪造并诱使用户A在不经意间点击这个链接,而在请求这个伪造的URL时,实际上是请求了某个需要用户A身份认证登录的服务。由于大部分登录服务都是通过Cookie来识别用户的,而这些Cookie存储在用户A登录过服务的浏览器上,只要不关闭浏览器不退出登录,以后访问这个服务就会带上这个Cookie直接登录 。
此漏洞就是利用保存在浏览器上的Cookie身份信息,不管以哪种方式请求对应的服务,都会自动读取Cookie免登录。尤其是在跨域情况下,虽然由于“跨域”无法用代码中的变量接收返回的内容(不管是Form表单的submit提交还是Ajax方式的提交),但如果不关心返回结果(不判断接口的请求结果是否成功),只是请求而已,此时就有可能在用户不知情的情况下修改了用户A对应的业务数据。
二、攻击条件:
1、需要用户登录,且用户身份认证信息存在浏览器的Cookie等可重复使用的地方;
2、Get查询接口URL一般不考虑,只考虑那些Post修改接口;
3、用户直接点击某链接就可以触发请求另一个接口URL;
三、攻击示例:
如果用户POST请求输入$userName和$passWord登录银行的某个有CSRF漏洞网站A:http://localhost:8000/bank.php,代码如下:
<?php
const USERNAME = 'abcd';
const PASSWORD = '123456';
//是否登录
$isLogin = false;
//模拟登录
$auth = $_COOKIE['auth'];
if ($auth) {
//模拟解密过程
$userInfo = explode(',', $auth);
if ($userInfo && count($userInfo) === 2) {
$userName = $userInfo[0];
$passWord = $userInfo[1];
} else {
die('auth失效,请重新登录!');
}
} else {
if ($_POST['userName'] && $_POST['passWord']) {
$userName = $_POST['userName'];
$passWord = $_POST['passWord'];
} else {
die('请输入用户名和密码登录!');
}
}
if ($userName === USERNAME && $passWord === PASSWORD) {
//模拟加密过程
$auth = $userName . ',' . $passWord;
//cookie24小时后过期
$expire = time() + 24 * 60 * 60;
setcookie('auth', $auth, $expire);
echo '登录成功!';
} else {
die('登录失败,请输入用户名和密码登录!');
}
// 用户登录成功后,进行密码修改、转帐等一系列操作
$newPwd = $_POST['newPwd'];
当用户$userName输入:abcd,$passWord输入:123456登录成功时,会在浏览器中设置Cookie:auth=abcd%2C123456。然后用户进行密码修改等操作。
当用户在未关闭浏览器且未退出帐号时,又点击了一个不可信任、不安全的网站B(与网站A不同的协议、HOST及端口): http://localhost:8001/danger.html,代码如下:
<html>
<head>
<meta charset="utf-8">
<title>Dangerous Page</title>
</head>
<body>
<!-- 不关心是否请求成功,只是请求,所以不存在跨域问题 -->
<form action="http://localhost:8000/bank.php" method="POST">
<input type="hidden" name="newPwd" value="任意密码,随便输" />
<input type="submit" value="提交" />
</form>
</body>
</html>
可以看到即使不用输密码,也可以修改密码。虽然是用户本人点击的链接,但并不是用户主观希望修改密码。
另外,对于一些特殊的、没有Post请求参数,只需用户认证登录就可修改数据的接口(比如用户清除所有未读消息,只需要用户身份信息修改所有未读消息的状态),粘在“同域”下的页面诱使用户点击,虽然此时并未“跨域”,但用户并不清楚这个URL的实际作用,也会在不经意间修改了自己的数据。
四、防御措施:
1、服务端使用Referer或Origin请求头判断请求来源,因为前端在发起请求时无法修改设置这两个请求头,只能是浏览器自动设置,参见:禁止修改的消息首部 - 术语表 | MDN。
说明:postman等模拟请求工具虽然可以修改Header头,但它只是模拟成了一个浏览器发起请求,实际浏览器并不能修改上述请求头。
2、保证某些请求参数必输,且这些参数不放在URL中,且这些参数不存储在浏览器可以重复读取利用的地方(比如Cookie中)。在这种情况下,必输参数只能放在Post参数中且该参数无法被恶意用户通过抓包等方式获取到,并且无法找到规律,请求时就会报缺少参数或参数错误无法访问服务。最常用的是使用身份令牌token:
这个token是与用户身份相关的随机数,且有有效期。每次请求都需要通过POST表单提交这个token,请求到服务端之后服务端要校验这个token值对不对。由于这个token没保存在Cookie中,只能通过Post参数传递,即使带上了Cookie中正确的身份信息,token缺失或者不正确都会直接被服务拒绝。这个token只有用户本身登录成功后才能拿到,别人无法伪造。
当然,如果用户自己从正确的请求页面中抓包拿到token,在一个新页面发请求,这种情况不考虑在内,毕竟这属于和用户密码泄露一样,并不能通过系统避免。
3、对于无Post必输参数的请求,单纯地增加Header头的Referer或Origin请求头校验,在“同域”下仍然不能避免诱使用户无感知点击链接,只能通过增加token的方式来解决。