CSRF/XSRF—Cross Site Request Forgery
跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。 一个典型的CSRF攻击有着如下的流程:
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
1. 几种常见的攻击类型
- GET类型的CSRF:GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般操作如下。
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
在受害者访问含有这个img的页面后,浏览器会自动发送发送一次http get请求,bank.example就会收到包含受害者登录信息的一次跨域请求。
- POST类型的CSRF:这种类型的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操作。
- 链接类型的CSRF:链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">重磅消息!!
<a/>
由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。
2. CSRF的特点
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。 CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
3. 防护策略
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。 上文中讲了CSRF的两个特点:
- CSRF(通常)发生在第三方域名。
- CSRF攻击者不能获取到Cookie等信息,只是使用。
针对这两点,可以专门制定防护策略,如下:
- 阻止不明外域的访问* 同源检测
- Samesite Cookie
- 提交时要求附加本域才能获取的信息* CSRF Token
- 双重Cookie验证
3.1 同源检测
同源检测的目的是阻止不安全的外域访问。 既然CSRF大多来自第三方网站,那么就直接禁止外域(或者不受信任的域名)对发起请求。 在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
- Origin Header
- Referer Header 因此服务端可以通过解析这两个Header中的域名,确定请求的来源域。 而且因为浏览器兼容性的缘故,Origin、Referer字段不一定都存在,一个原则是当Origin存在时直接使用Origin中的字段确认来源域名就可以。 2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,因此开发者终于可以灵活地控制自己网站的Referer策略了。设置Referrer Policy的方法有三种:
- 在CSP设置
- 页面头部增加meta标签
- a标签、img标签增加referrerpolicy属性 根据上面内容,可以知道一个问题:攻击者可以在自己的请求中隐藏Referer。如果攻击者将自己的请求这样填写:
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
那么这个请求发起的攻击将不携带Referer。 另外在以下情况下Referer没有或者不可信:
1.IE6、7下使用window.location.href=url进行界面的跳转,会丢失Referer。
2.IE6、7下使用window.open,也会缺失Referer。
3.HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失。
4.点击Flash上到达另外一个网站的时候,Referer的情况就比较杂乱,不太可信。 所以当Origin和Referer头文件不存在时,建议直接进行阻止,特别是如果您没有使用随机CSRF Token(参考下方)作为第二次检查。
3.2 外域访问例外情况
当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似CSRF攻击。所以在判断的时候需要过滤掉页面请求情况,通常Header符合以下情况:
Accept: text/html
Method: GET
但相应的,页面请求就暴露在了CSRF的攻击范围之中。如果你的网站中,在页面的GET请求中对当前用户做了什么操作的话,防范就失效了。 所以,同源策略还要求开发者不使用get请求进行修改操作,get请求只能进行简单查询操作!
3.3 CSRF Token
CSRF Token防范攻击的基础是:攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。 因此可以让所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。 CSRF Token的防护策略分为三个步骤,下面将逐一讲述。
3.3.1 将CSRF Token输出到页面
首先,用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合。 显然在提交时Token不能再放在Cookie中,否则又会被攻击者冒用。因此,为了安全起见Token最好还是存在服务器的Session中,之后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。
所以Token由服务端生成,并且以安全的方式返回到前端(一般是登录时随登录信息返回)。然后前端存储该Token,注意绝对不能将Token存储在Cookie中,否则又会被攻击者冒用。之后前端发起的每个请求都需要添加Token(Get/Post/Put/Delete)
全局添加token可以通过request filter实现,比如axios提供了 axios.interceptors.request.use 实现对所有请求的过滤
3.3.2 页面提交的请求携带这个Token
对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
这样,就把Token以参数的形式加入请求了。
3.3.3 服务器验证Token是否正确
当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。这种方法要比之前检查Referer或者Origin要安全一些,Token可以在产生并放于Session之中,然后在每次请求时把Token从Session中拿出,与请求中的Token进行比对,但这种方法的比较麻烦的在于如何把Token以参数的形式加入请求。
建议使用request实例请求过滤器增加自定义token,不能全局拦截整个站点的请求,否则会将攻击请求也添加上token,造成防范机制失效
通常,开发人员只需为当前会话生成一次Token。在初始生成此Token之后,该值将存储在会话中,并用于每个后续请求,直到会话过期。当最终用户发出请求时,服务器端必须验证请求中Token的存在性和有效性,与会话中找到的Token相比较。如果在请求中找不到Token,或者提供的值与会话中的值不匹配,则应中止请求,应重置Token并将事件记录为正在进行的潜在CSRF攻击。
3.3.4 分布式校验Token
现在大型网站采用分布式部署,用户发起的HTTP请求通常要经过像Ngnix之类的负载均衡器之后,再路由到具体的服务器上,由于Session默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿到之前的HTTP请求存储在服务器中的Session数据,从而使得Session机制在分布式环境下失效。解决办法是:在分布式集群中CSRF Token需要存储在Redis之类的公共存储空间。由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题,目前很多网站采用Encrypted Token Pattern方式。这种方法的Token是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的Token,只用再次计算一次即可。解决分布式环境session共享问题:(session共享也会影响到Token的共享)
1.使用公共存储服务redis存储session,实现session共享
2.使用Encrypted Token Pattern方式计算得到Token
3.3.5 Token防范总结
Token是一个比较有效的CSRF防护方法,只要页面没有XSS漏洞泄露Token,那么接口的CSRF攻击就无法成功。 但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个Form及Ajax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。
4. 双重Cookie
4.1 实现原理
利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值,该值没有业务意义,作用仅仅是进行安全校验。双重Cookie采用以下流程:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。
- 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST www.a.com/comment?csr…
- 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。 当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因举例说明如下。 由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
- 如果用户访问的网站为www.a.com,而后端的api域名为api.a.com。那么在www.a.com下,前端拿不到api.a.com的Cookie,也就无法完成双重Cookie认证。
- 于是这个认证Cookie必须被种在a.com下,这样每个子域都可以访问。
- 任何一个子域都可以修改a.com下的Cookie。
- 某个子域名存在漏洞被XSS攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的Cookie。
- 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向www.a.com下,发起CSRF攻击。
4.2 双重Cookie优点
用双重Cookie防御CSRF的优点:
- 无需使用Session,适用面更广,易于实施。
- Token储存于客户端中,不会给服务器带来压力。
- 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。
4.3 双重Cookie缺点
缺点:
- Cookie中增加了额外的字段。
- 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
- 难以做到子域名的隔离。
- 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。
5. Samesite Cookie属性
为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。
5.1 Samesite=Strict
这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:
Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会。举个实际的例子就是,假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite = Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个 Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。
5.2 Samesite=Lax
这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求,则这个Cookie可以作为第三方Cookie。比如说 b.com设置了如下Cookie:
Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3
当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则bar也不会发送。
6.防止攻击
前面所说的,都是被攻击的网站如何做好防护。而非防止攻击的发生,CSRF的攻击可以来自:
- 攻击者自己的网站。
- 有文件上传漏洞的网站。
- 第三方论坛等用户内容。
- 被攻击网站自己的评论功能等。 对于来自黑客自己的网站,目前无法防护。但对其他情况,那么如何防止自己的网站被利用成为攻击的源头呢?
- 严格管理所有的上传接口,防止任何预期之外的上传内容(例如HTML)。
- 添加Header X-Content-Type-Options: nosniff 防止黑客上传HTML内容的资源(例如图片)被解析为网页。
- 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。
- 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)。
总结
- CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
- CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
- 保证页面的幂等性,后端接口不要在GET请求中做编辑操作(增、删、改操作)。