CSRF全称是跨站请求伪造(cross site request forgery),CSRF伪装受信任用户,向第三方平台发送恶意请求。
CRSF能做的事情包括利用你的身份发邮件,发短信,进行交易转账,甚至盗取账号信息。
一个典型的CSRF攻击有着如下的流程:
受害者登录 a.com,并保留了登录凭证(Cookie)。
攻击者引诱受害者访问了 b.com。
b.com 向 a.com 发送了一个请求:a.com/act=xx 。浏览器会…
a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
a.com以受害者的名义执行了act=xx。
攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
GET类型的CSRF
GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
POST类型的CSRF
这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
重磅消息!!
CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。
CSRF的两个特点:
针对这两点,我们可以专门制定防护策略,如下:
既然CSRF大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。
在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。
服务器可以通过解析这两个Header中的域名,确定请求的来源域。
在部分与CSRF有关的请求中,请求的Header中会携带Origin字段。字段内包含请求的域名(不包含path及query)。
如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。
但是Origin在以下两种情况下并不存在:
根据HTTP协议,在HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址。
对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。 对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。
这种方法并非万无一失,Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。
2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。
新版的Referrer Policy规定的五种Referer策略与之前存在的三种策略(never、default和always)的对应关系如下:
策略名称 | 属性值(新) | 属性值(旧) |
---|---|---|
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 Policy的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer
设置Referrer Policy的方法有三种:
如果攻击者将自己的请求这样填写,可以在自己的请求中隐藏Referer。
另外在以下情况下Referer没有或者不可信:
如果Origin和Referer都不存在,建议直接进行阻止,特别是如果您没有使用随机CSRF Token(参考下方)作为第二次检查。
通过Header的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域,或者子域名,或者有授权的第三方域名,又或者来自不可信的未知域名。
当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似CSRF攻击。所以在判断的时候需要过滤掉页面请求情况,通常Header符合以下情况:
Accept: text/html
Method: GET
但相应的,页面请求就暴露在了CSRF的攻击范围之中。如果你的网站中,在页面的GET请求中对当前用户做了什么操作的话,防范就失效了。
另外,前面说过,CSRF大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。
综上所述:同源验证是一个相对简单的防范方法,能够防范绝大多数的CSRF攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。
前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。
而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
CSRF Token的防护策略分为三个步骤:
将CSRF Token输出到页面中
首先,用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了,否则又会被攻击者冒用。因此,为了安全起见Token最好还是存在服务器的Session中,之后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。
页面提交的请求携带这个Token
对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue
。 而对于 POST 请求来说,要在 form 的最后加上:
复制代码这样,就把Token以参数的形式加入请求了。
服务器验证Token是否正确
当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。
这种方法要比之前检查Referer或者Origin要安全一些,Token可以在产生并放于Session之中,然后在每次请求时把Token从Session中拿出,与请求中的Token进行比对,但这种方法的比较麻烦的在于如何把Token以参数的形式加入请求。
这个Token的值必须是随机生成的,这样它就不会被攻击者猜到,考虑利用Java应用程序的java.security.SecureRandom类来生成足够长的随机标记,替代生成算法包括使用256位BASE64编码哈希,选择这种生成算法的开发人员必须确保在散列数据中使用随机性和唯一性来生成随机标识。
通常,开发人员只需为当前会话生成一次Token。在初始生成此Token之后,该值将存储在会话中,并用于每个后续请求,直到会话过期。当最终用户发出请求时,服务器端必须验证请求中Token的存在性和有效性,与会话中找到的Token相比较。如果在请求中找不到Token,或者提供的值与会话中的值不匹配,则应中止请求,应重置Token并将事件记录为正在进行的潜在CSRF攻击。
在大型网站中,使用Session存储CSRF Token会带来很大的压力。访问单台服务器session是同一个。但是现在的大型网站中,我们的服务器通常不止一台,可能是几十台甚至几百台之多,甚至多个机房都可能在不同的省份,用户发起的HTTP请求通常要经过像Ngnix之类的负载均衡器之后,再路由到具体的服务器上,由于Session默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿到之前的HTTP请求存储在服务器中的Session数据,从而使得Session机制在分布式环境下失效,因此在分布式集群中CSRF Token需要存储在Redis之类的公共存储空间。
由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题,目前很多网站采用Encrypted Token Pattern方式。这种方法的Token是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的Token,只用再次计算一次即可。
这种Token的值通常是使用UserID、时间戳和随机数,通过加密的方法生成。这样既可以保证分布式服务的Token一致,又能保证Token不容易被破解。
在token解密成功之后,服务器可以访问解析值,Token中包含的UserID和时间戳将会被拿来被验证有效性,将UserID与当前登录的UserID进行比较,并将时间戳与当前时间进行比较。
另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。
双重Cookie采用以下流程:
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。
当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高
由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
用双重Cookie防御CSRF的优点:
缺点:
为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax,下面分别讲解:
Set-Cookie: foo=1; Samesite=Strict //Cookie不会被包含在Cookie请求头中
Set-Cookie: bar=2; Samesite=Lax//Cookie会被包含在Cookie请求头中
Set-Cookie: baz=3
如果请求发起的是异步请求,或者页面跳转是通过表单的post提交触发的,bar也不会被包含在Cookie请求头中
如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。
但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。
如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。
另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版Chrome和Firefox支持以外,Safari以及iOS Safari都还不支持,现阶段看来暂时还不能普及。
而且,SamesiteCookie目前有一个致命的缺陷:不支持子域。例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。
总之,SamesiteCookie是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望。
若有错误,希望大神们指点指点 ?
前端安全系列之二:如何防止CSRF攻击