场景模拟
用户在登录B站的前提下,点击了黑客送给它的A站的链接。A站的页面中包含一个对B站资源的请求,于是用户浏览器向A站发起请求,并且附带上B站发的cookie,这就达到了冒充用户身份实现某种操作的攻击意图。
类型:GET型、POST型、链接型
防御办法:
阻止不可信外域的访问
- 检验
Origin
、Referrer
这两个HTTP Header
在大多数情况下,浏览器在发送请求时会携带它们,并且js无法修改它们。但在少数情况下, 浏览器不会发送它们:
Origin
值为发起请求的页面的URL地址(不含path即query);但IE11不会在跨站请求中加上它,另外,302重定向时所有浏览器也不会加它。
Referrer
在ajax、图片和script请求中值是HTTP请求的来源地址,在页面跳转的情况下,是进入最终页面的前一个跳转页面的地址;但在某些情况下,HTTP请求也不含Referrer
,如HTTPS转HTTP页面,或使用含有漏洞/可修改HTTP Header的浏览器,或使用IE6/7的部分js API。
W3C 新修的草案里为Referrer Policy
提供了5种值:
策略名称 策略值(新) 对应旧值 No Referrer no-referrer never No Referrer When Downgrade no-referrer-when-downgrade default Origin Only (same or strict) origin origin Origin When Cross Origin (strict) origin-when-crossorigin — Unsafe URL unsafe-url always
Referrer
可在三个地方设置:
- CSP
- HTML页面的
<meta>
标签<a>
类标签添加referrerpolicy
属性,如:<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
思考: 借助这俩Header验证的本质是依赖第三方(浏览器)的安全,但在某些特殊情况下(如用户使用老旧浏览器、多级重定向跳转等),Referrer
的值不足以区分正常请求和恶意请求,此时需要其他验证手段辅助;另外,如果浏览器确定可信,那也可以引入HTTP2.0新加的Sec-Fetch头,它可以提供更加细致的请求描述(请求对象、请求类型、请求模式) ,使用它们做验证依据可靠性更高。P.S.当然,这么做的前提是HTTP2.0普及,目前还需时日。
- 设置Cookie的Samesite属性
google的草案里为cookie添加了
Samesite
字段,它有三个值Strict
和Lax
、None
,它是两种级别的同源限制;以链接跳转为例,当登录Cookie的该属性设为Strict
时,当你从百度搜索页跳到淘宝页,将不会自动登录,因为cookie没被发送;当值为Lax
时,在链接跳转的情形可以发送cookie,因此可以自动登录,但在img、iframe直接请求外域资源时不会发送cookie;当值为None
时取消所有限制,即只要域名对的上就会发送cookie(这里隐藏条件是必须增加Secure
属性)。
防范CSRF来说,该草案很够用,但也存在一些弊端:
- 当设置为
Strict
时,子域名跳转或打开新标签重进网站,都不会携带Cookie。这样的用户体验很差。- 致命问题:不支持子域,如在
top.a.com
下无法使用domain字段为a.com
的Cookie,这在有多个子域名时会很麻烦。
本域凭证校验(CSRF Token)
方式一(token备份在session中):
服务器在用户cookie的有效周期内,为它生成一个token(通常是:
hash( salt + timestamp )
),并将备份存入session。如果是后端控制页面的情况,则可以在用户请求页面时直接将token放入表单发给用户,在用户提交表单时进行验证;在后端不可修改页面HTML的情况,可以借助cookie、响应头等媒介,让前端存储下token,在提交表单或ajax请求时动态添加一下。在请求到达服务器后,再和session中的token比较。
方式二(token备份在cookie中):
服务器在生成token后直接并将备份写入cookie发给前端,前端在ajax或提交表单时提取出token(因为在CSRF攻击中黑客无法读取cookie的值)放在请求中(URL、请求头、body),由于cookie也会一并发送给后端,后端在校验时比较二者是否相同。
说明: 方式二的实施成本比方式一要低,但在大型网站上却使用的不多。原因主要在于,为了让不同的子域名能够获取到种了token的cookie,该cookie的域必须设置成较高级别的(比如二级域名),这会给大型网站暴露出更多风险。因为每个子域名下的js都可以修改这条cookie,倘若某个子域名存在XSS漏洞(大型网站的子域名好多的,有些使用老旧的技术开发),则黑客可以借此突破当前域名下的CSRF防御。
思考: 一般来说,token的生命周期和表单一致,当一个表单提交后该条token就被消耗掉,我们可以从token池中获取并分配下一条token,这种设计常用于方式一中;当使用方式二时,因为服务端没有留存生成token的时间戳,不好校验它的有效性,所以可将token的生命周期延长为用户登录cookie的有效期。或者放弃使用哈希运算,换成加解密运算的方式,即可完成有效性的校验(服务端接收到请求的时间 - 解密后获得的生成时间),Django框架正是采用了这种方式(它自写了一个简单的加解密算法)。但因为将哈希运算换成了加解密运算,无疑增加了服务器CPU的开销。
设计案例
Spring Security
Spring Security提供了3个CsrfTokenRepository,其中HttpSessionCsrfToken和CookieCsrfToken顾明思议,用于方式一、方式二的场景,LazyCsrfToken提供了程序员自写方法的接口。前两种token的生成方式十分简单:
private String createNewToken() {
return UUID.randomUUID().toString();
}
验证逻辑在org.springframework.security.web.csrf.CsrfFilter#doFilterInternal
中,流程如下:
Django
如下图,为Django中的CSRF验证逻辑:
其中,csrftoken
从请求的Cookie中获取,csrfmiddlewaretoken
从POST表单或请求头中获取,通过判断二者解密后的secret值来决定是否为CSRF攻击。
参考阅读: