CSRF全称为Cross Site Request Forgery,译为跨站请求伪造。CSRF指攻击者盗用了用户的身份,以用户的名义在其不知情的情况下发送恶意请求。CSRF的常见攻击常见包括:以用户的名义发送邮件、发送消息、购买商品、虚拟货币转账等。
HTTP请求是无状态的,也就是说每次HTTP请求都是独立的无关之前的操作的,但是每次HTTP请求都会将本域下的所有cookie作为HTTP请求头的一部分发送给服务端,所以服务端就根据请求中的cookie执行相关的操作,达到了伪造者使用用户的身份的目的。

常见的CSRF攻击类型有:GET类型的CSRF、POST类型的CSRF和链接类型的CSRF
GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
<img src="http://bank.example/withdraw?amount=10000&for=hacker">
在受害者访问含有这个img的页面后,浏览器会自动向
http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker
发出一次HTTP请求,bank.example就会收到包含受害者登录信息的一次跨域请求。
这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST>
<input type="hidden" name="account" value="xiaoming" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
form>
<script> document.forms[0].submit(); script>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">重磅消息!!<a/>
由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功
Referer可以用来检查请求是否来自于合法的源。常见的互联网应用中,页面与页面之间都具有一定的逻辑关系,这就使得每个正常请求的referer具有一定的规律。
这种方法并非万无一失,Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。
下述为Referer检查的一种实现,为了避免来自客户端的请求被拦截,此处WARN日志观察,亦可校验对应的UserAgent来判断自客户端的请求
/**
* 校验非法Referer
* 0-无法判断 CODE_CSRF-非法
*
* @param request
* @return
*/
private int checkReferer(HttpServletRequest request) {
String referer = request.getHeader("Referer");
try {
// 非法Referer校验
if (referer == null) {
logger.warn("无Referer, referer is null, uri:{}", request.getRequestURI());
return 0;
}
URI uri = new URI(referer);
String host = uri.getHost().toLowerCase();
// 过滤非example.com & ext.com过来的请求
if (!host.endsWith(".example.com") && !host.endsWith(".ext.com")) {
logger.warn("非法Referer, referer:{}", referer);
return CODE_CSRF;
}
} catch (Exception e) {
logger.error("非法Referer, referer:{}", referer);
if(referer != null && referer.startsWith("https://www.example.com/")){
return 0;
}
return CODE_CSRF;
}
return 0;
}
CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于cookie中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的cookie进行安全验证。
因此,抵御CSRF攻击的关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于cookie中。因此,可以在http请求中以参数或自定义Header的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝该请求。
SameSite属性用于定义在进行跨站请求时是否带上cookie,在Chrome51之后被引入到Chrome中,90%的用户所使用的浏览器支持这个属性,它可以设置三个属性:Strict、Lax以及None。当设置为Strict时最为严格,在进行任何跨站操作时均不发送cookie,即便是通过点击普通的a链接进行跳转,也不会带上cookie。Lax则相对宽松,链接、预加载请求以及GET表单进行跨站跳转时会发送cookie,Chrome80之后Chrome默认是Lax属性。None相当于是关闭SameSite功能。
| 类型 | 实例 | Strict | Lax | None |
|---|---|---|---|---|
| 链接 | |
不发送cookie | 发送 | 发送 |
| 预加载 | |
不发送cookie | 发送 | 发送 |
| GET表单 | |
不发送cookie | 发送 | 发送 |
| POST表单 | |
不发送cookie | 不发送 | 发送 |
| iframe | |
不发送cookie | 不发送 | 发送 |
| ajax | $.get("...") |
不发送cookie | 不发送 | 发送 |
| img | |
不发送cookie | 不发送 | 发送 |
参考资料: