什么是CSRF攻击?
之前我们介绍了前端攻击中的XSS攻击,它主要是黑客将恶意脚本通过各种手段注入到服务器或者在HTML页面传输的过程中劫持然后修改DOM注入恶意脚本,当浏览器加载脚本的时候恶意脚本也会一起被加载,然后恶意脚本执行对用户隐私信息如cookie的收集,然后通过Ajax配合CORS将cookie发送到黑客服务器,然后伪装用户完成攻击。
这次要介绍的是前端攻击中另外一个十分重要的攻击方式--CSRF攻击。CSRF攻击全称为Cross Site Request Forgery,也称作跨站请求伪造。
主要是指黑客诱导用户点击跳转到黑客的网站,在用户进入黑客网站后无感知的情况下,黑客发起对于源站的跨站请求,由于用户的登录状态还未失效,所以请求到达源站时服务器在校验用户登录状态的时候会通过,黑客就可以伪装用户发起转账等恶意操作。
CSRF攻击会对源站点有何影响
一般来说,黑客完成CSRF攻击之后,会伪装用户做这些事情:
利用用户已认证的身份信息,修改用户在当前站点的配置和设定信息。
一个著名的案例是2007年的Gmail邮箱攻击事件。用户在登录Gamil邮箱之后点击了另外一份邮件中的恶意链接,该链接跳转的地址就是黑客网站,由于当时该用户的登录状态还未过期,所以黑客就伪造用户的身份向Gmail服务器重新设置了该用户的邮件过滤功能,该功能是将用户的所有邮件都转发到黑客的邮箱上,这是一个典型的点击陌生点击造成的CSRF攻击。
利用用户已认证的身份信息完成转账购买商品等账户交易
利用用户已认证的身份信息在诸如UGC网站的评论区发布恶意链接等
CSRF常见的三种攻击方式
自动发起GET请求
黑客提前在第三方网站存好这样一张img图片:
<img src="https://bank.com/sendmoney?amount=1000&user=hacker" />
当用户点击链接进入此页面之后,会在不知觉的情况下向源站点发送一条转账的HTTP请求,银行站点就会收到包含用户登录信息的一条转账请求。
自动发起POST请求
黑客提前在第三方网站设置好一个隐藏的自动提交表单如下:
<!DOCTYPE html>
<form id='hacker-form' action="https://bank.com/sendmoney" method=POST>
<input type="hidden" name="user" value="hacker" />
<input type="hidden" name="number" value="1000" />
</form>
<script> document.getElementById('hacker-form').submit(); </script>
</body>
</html>
用户在进入黑客网站之后,就会向源站点发送一条包含个人登录信息的转账POST请求,银行站点校验身份信息通过之后就会发起转账。
虽然说POST比GET请求方式要复杂一些,但是完成攻击还是非常容易,所以后端不能将安全寄托在POST接口上面。
诱导用户点击链接的CSRF攻击
这类攻击往往发生在UGC类型的网站或者论坛上,这类型网站可以允许用户自己发布信息,然后服务器会将用户发布的信息展示在页面上,如果没有做好CSRF防护的话,黑客就可以在发布图片或者链接的时候嵌入恶意链接,当其他用户看到黑客发布的图片或者点击了黑客发布的链接的那一刻,CSRF攻击就发生了,比如:
<a href="https://bank.com/sendmoney?amount=1000&user=hacker" target="_blank">特大新闻!!!点我查看!!!<a/>
CSRF攻击过程
一个典型的CSRF攻击过程如下:
用户A(被攻击者)登录了某银行网站(B.com),登录成功之后服务端返回了A一个cookie,里面保存的是用户的session_id等身份信息;
用户A受黑客引诱无意间点击了黑客早已准备好的第三方网站(C.com)
黑客在C.com中提前准备写好了自动提交请求的脚本,只要用户进入此黑客网站,请求就会发送(不是通过Ajax请求发送)
C.com向银行网站B.com发送了一个转账的请求,由于是向B.com发送的请求所以浏览器会自动在请求头中带上domain=B.com的cookie;
请求到达B网站的服务器之后,进行校验,校验发现携带的cookie中session_id去session库中匹配成功,身份校验通过
B网站以为是用户A自己在操作,所以执行本次请求,完成对于黑客账户的转账
攻击完成,黑客在受害者用户A不知情的情况下,冒充受害者让银行网站执行了自己的操作。
CSRF攻击的特点
通过上诉对一个完整的CSRF攻击过程的描述,我们发现CSRF攻击要想完成攻击,有一些特点:
CSRF攻击多数情况下发起与第三方网站(跨域);
CSRF攻击少数情况下发起于当前网站,比如可以提交图片和链接的评论区,这种攻击可以在当前域发起,更加难以追踪;
CSRF攻击的攻击对象必须是已经完成登录的用户,并且此用户的登录状态还保持在浏览器端没有过期;
CSRF攻击的整个过程中黑客并不能获取受害者的用户凭证,只是冒用。这一点是XSS可以直接获取用户cookie等信息不同。
CSRF攻击的手段多样化,可以通过图片URL、超链接、CORS等多种情况发起攻击
如何阻止CSRF攻击?
这里特地说明下:
用户个人是否点击某个链接这样的行为我们是无法约束的,我们只能假设用户已经点击链接并进入黑客页面之后,该如何通过技术手段来防止后续对于用户隐私和财产的损失。
任何一种防范手段都不能100%的阻止攻击的发生,只能尽可能的提高网站的安全能力.后续对于如何阻止CSRF攻击的方式也是基于此前提来进行的。
通过上面对于CSRF攻击特点的分析,我们明白了攻击发生的前提条件和特点,主要就是两个大的方向:
CSRF攻击大多数发生在第三方站点,由第三方站点发起携带着用户登录状态的跨域请求(当然也有少部分是当前页面的比如UGC评论区发布的恶意链接)
CSRF攻击利用了用户保存在浏览器中尚未过期的登录状态如cookie等信息,但只是利用并不能获取。
那么我们的防护策略也应该做针对性的部署:
既然攻击多数发生在第三方站点,那我就要求服务器禁止一切不信任站点的跨域访问
基于同源策略确定请求发起的站点是否为源站点
基于cookie的SameSite属性
既然攻击发生时利用的是用户的身份凭证,那我就要求在请求时必须携带只有源站点才有的信息才可以通过身份验证,因为cookie是浏览器自动携带的,但是其他信息浏览器不能自动携带,并且CSRF的特点是无法获取源站点的信息。
CSRF Token
下面我们就来一一详细介绍写这些阻止CSRF攻击的策略。
基于同源策略确定请求发起的站点是否为源站点
同源策略防止CSRF攻击的原理是:既然绝大多数的CSRF攻击都发生在第三方的域名,那源站点的服务器就需要对来自于其他域的请求禁止。
那么服务器该如何判断一个请求来源自那个域呢?
答案是HTTP的请求头字段Origin和Referer,HTTP规定每一个请求都会携带两个请求头字段Origin和Referer用于告知服务器当前请求发起的来源,该字段由浏览器自动携带,前端无法进行自定义,服务器可以通过解析当前请求的Origin和Referer请求头中携带的域名,进一步确定请求来源。
通过请求头字段Origin获取
绝大多数情况下这个字段都可以获取到请求来源域名,但是下面两个特殊情况无法获取:
IE11不会在跨站的CORS请求上添加Origin字段
302重定向之后Origin不会包含在重定向的请求中,因为浏览器不想将Origin泄漏到除源站以外的重定向站点上
通过请求头字段Referer获取
对于Ajax请求、图片和script等静态资源请求,Referer字段为发起请求的站点域名;
对于页面跳转,Referer字段为Open页面的历史记录的前一个页面的域名。
值得一提的是,Referer字段中包含了请求来源站点的域名以及path地址,而Origin字段则只包含了源站点的域名:
origin: https://mp.weixin.qq.com
referer: https://mp.weixin.qq.com/s/sYoccR4-qM4crgkQBYvSpA
但是如果服务器单纯的依靠Referer字段的值去校验当前请求的源站时,其实也是不可靠的。因为Referer字段的值是浏览器在请求发起的时候自动填入的,并不能保证浏览器安全没有漏洞。
后来W3C组织发布Referer Policy草案,详细的说明了对浏览器发送Referer字段的值做了详细的规定。新版本的Referer Policy中包含下列五种策略:
No Referrer:对应属性值 No Referrer
No Referrer When Downgrade:对应属性值 No Referrer When downgrade
Origin Only:对应属性值same origin或者strict origin
Origin When Cross-origin:对应属性值origin When Cross-origin
Unsafe URL:对应属性值unsafe URL:
设置Referer Policy的方式主要有三种:
通过CSP设置(后续会详细说)
通过页面头部的meta标签设置
a标签设置referrerpolicy属性
比如前端可以将Referer Policy设置为same origin,只要发起请求的站点同源时会在请求头中加上Referer字段;如果是跨域请求就不会携带Referer字段。
综上所述,通过同源策略中的Origin和Referer字段是可以有效限制来自第三方站点的攻击的,但是如果CSRF攻击发生在本域的话,这种方法就无法有效规避,所以对于安全性要求高或者有用户输入UGC的网站,就需要使用更加完善的策略来防止CSRF攻击。
CSRF Token
之前说到CSRF攻击往往是冒用用户cookie中携带的身份信息,但是攻击者并不能真正获取到用户信息。基于这一点就产生了CSRF Token的防护措施。
CSRF Token的基本原理
我们可以要求所有的用户在登录之后的每一个请求都携带一个Token,这个Token是服务器派发的需要用户在请求的时候进行携带,在请求到达服务端的时候,服务器需要对CSRF Token进行校验,校验通过才可以执行后续的业余操作,由于攻击者无法真正的获取到用户的信息,所以攻击者发起的请求中是无法获取到CSRF Token的,因此可以防范CSRF攻击。
CSRF Token的防范过程
用户登录站点,服务器基于加密算法、随机数和时间戳生成一个Token之后,一边将其放入session库,一边将其返回给前端
浏览器收到Tokne,将其存放在localStorage中 不能放在cookie中防止被自动发送
浏览器此后对站点的请求,都会在请求头中携带上此token,发送给服务端
服务端获取到前端发来的Token,对Token进行解密然后校验token中信息是否匹配,比如用户id,过期时间等,如果一致那么处理后续业务
我们常见的银行网站在已登录的状态下转账还需要发送验证码和密码的时候,验证码和密码就可以看做是一个CSRF Token。
CSRF Token的优点
CSRF Token在目前使用的方案就是JWT,服务器在获取到JWT之后只需要使用密钥进行解密,解密之后的信息中就包含了用户信息和过期信息等,也就是不需要再像session一样拿去数据库进行对比校验,而是自己就可以完成校验,也就是JWT既可以防范CSRF攻击,还可以携带用户信息,又解决了session分布式存储的难点,所以是一种比较流行的方案。
基于cookie的SameSite属性
为了彻底从源头上解决CSRF攻击的问题,HTTP规范新增了一个cookie的属性:SameSite。
当服务器向客户端返回并设置一条cookie的时候,可以通过samesite属性来标记这个cookie是一个同站的cookie,不可用作第三方cookie。
Set-Cookie:uid=12345;HttpOnly;SameSite=Strict;
Set-Cookie:uid=12345;HttpOnly;SameSite=Lax;
SameSite的值为strict
当SameSite的值为strict的时候,表示为严格模式。
意思就是除了当前源站之外,其他任意网站对该服务端发起的请求都会携带此cookie。
这样以来就绝对性的保证了从任何第三方发起的CSRF攻击都因为无法携带cookie,从而导致请求失败。
SameSite的值为Lax
Lax表示宽松模式。意思就是服务器告诉浏览器,如果发起的请求为从其他页面跳转到当前站点并且为GET类型的请求,那么这个cookie可以在请求的时候被携带。
具体来说就是假设我们的源站是B.com,当我们从其他网站跳转到B.com的时候不是会去请求B.com页面所需的资源吗?这时候的请求是可以携带cookie的。
但如果是其他网站发起的对于B.com的异步Ajax请求,那么cookie也不会被携带,这里就涉及到跨域如何携带cookie的问题了?简单讲就是利用CORS策略中的Access-Control-Allow-Credentials:true这个响应头来配合客户端在发起请求时的withCredentials:true方法来携带,但是这里不属于CSRF讨论的范围,所以不再赘述。
SameSite最大问题在于设置严格的话,浏览器在任何跨域请求都不会携带cookie,就算新标签页打开页面都不会携带,这就会导致两个尴尬的问题:
当前域名登录页面之后,新打开一个标签页进入页面登录状态消失;
当前域名跳转到自己公司名下的子域名,登录状态也会消失,也就是无法单点登录。比如种在www.baidu.com下的samsite cookie,无法在www.news.baidu.com下携带。
总之,SameSite是一个解决的方案,但是还需要继续观察,目前应用场景还不是很成熟。
如何防止自己负责的页面被CSRF攻击?
在日常的项目开发中,我们应该从哪些方面考虑,防止自己负责的项目或者页面成为CSRF攻击的目标呢?
服务端需要对所有上传的接口做接口校验,禁止用户上传HTML页面等内容到服务器
对于用户上传的图片,进行转存或者校验,不要直接将用户提交的图片地址或者链接;
在论坛页面,当用户打开其他用户的链接要跳转的时候需告知用户风险,一部分是为了留存用户,另外一部分也是为了安全。这也是很多论坛不让发布外域的链接的原因。
涉及到用户隐私操作,需要验证码或用户密码