前端安全之 CSRF 攻击原理和防护方法

CSRF(Cross-site request forgery)简称:跨站请求伪造,学习 CSRF 攻击原理和防护方法是我们团队新成员的必修课,通常我都是先让新同学自己研究自己讲,然后我再通过其中细节再给他们讲一遍,讲的次数多了,也慢慢总结出一种比较容易理解的讲法。这里我整理成文分享给有需要的人。

引言:必修课为什么选择 CSRF?

  • CSRF 涉及到的前端知识点比较多,全面理解需要系统的学习 Cookie,前端跨域,HTTP 协议,web 浏览器等知识。

  • 理解 CSRF 攻击和防护方法是前端进阶要去,我也希望大家都能够掌握。

  • 个人兴趣,我个人对前端性能优化和前端安全比较感兴趣,也算参与和推动了公司内的 CSRF 防护方案从无到有的过程。

一、CSRF 攻击原理

1、Cookie 的使用

HTTP 是无状态协议,服务器只能根据当前请求的参数(包括 Head 和 Body 的数据)来判断本次请求需要达到的目的(Get 或者 Post 都一样),服务器并不知道这个请求之前干了什么事,所以这就是无状态协议。

但是我们现实很多情况需要有状态,比如登录态的身份信息,网页上很多操作需要登录之后才能操作,比如相册的上传照片和删除照片等功能都需要登录之后才能操作。那么怎么判断登录态?(通常我会问如果你遇到这个问题你回怎么做?),其实解决方案都是类似的,只能是每次 HTTP 请求都要把登录态信息(这里用 Key 表示)传给后台服务器,后台通过 Key 字段是判断用户合法性之后再处理这个请求要处理的敏感操作。

怎么方便的让每次 HTTP 请求都带上 Key?所以就设计出了 Cookie,这里列举 Cookie 主要的一些特性。

  • 浏览器默认自动携带本次 HTTP 请求域名的 Cookie(不管是通过什么方式,在什么页面发送的 HTTP 请求)。

  • 读写 Cookie 有跨域限制(作用域,Domain,Path)。

  • 生命周期(会话 or 持久)。

2、CSRF 的攻击过程

根据上面介绍,登录态 Cookie 的 Key 是浏览器默认自动携带的,Key 通常是会话 Cookie,只要浏览器不关闭,Key 一直存在。所以只要用户 A 曾经登录过相册网站(这里用 http://www.photo.com 举例),浏览器没有关闭,用户在没有关闭的浏览器打开一个黑客网页(这里用 http://www.hacker.com),黑客页面发送 HTTP 请求到 http://www.photo.com 的后台会默认带上 http://www.photo.com 的登录态 Cookie,也就能模拟用户 A 做一些增删改等敏感操作。Get 和 Post 都一样,这就是 CSRF 攻击原理。这种攻击过程也是最常见的攻击过程,后面还会介绍另外一种少见的攻击过程。

3、读操作能否被攻击到?

上面说的增删改都是写操作,会对后台数据产生负面影响,所以是能被攻击的。另外一种读操作,是具有幂等性,不会对后台数据残生负面影响,能否被攻击到?读操作也可能是敏感数据,举个例子,比如www.photo.com上的私密相册数据能否被www.hacker.com页面拿到?这就涉及到前端跨域知识点了,默认大部分情况是拿不到,这里列举两种特殊情是可以拿到的:

  • 如果后台返回的数据是 JSONP 格式的,这种只能是 Get 操作,是能被黑客页面拿到的。

  • 如果后台是通过 CORS 处理跨域,没有对请求头的 Origin 做白名单限制,ACAO 响应头是*或者包括黑客页面,包括 Get/Post/Del 等操作,也是能被黑客页面拿到的。

除了这两种特殊情况,读操作都是不能被攻击到的,因为浏览器跨域限制是天然的安全的。

二、CSRF 防护方法

知道攻击原理,防护方法也很简单,找到能够区分请求发送的页面是自己的页面还是黑客的页面的方法就可以了。

1、Referrer

HTTP 请求头 Referrer 字段是浏览器默认带上,含义是发送请求的页面地址,比如同样是删除相册的操作http://www.photo.com/del?id=xxx;如果是从相册自己页面发送出来,Referrre的值是http://www.photo.com/index.html(以首页举例),如果是从黑客页面发送出来的Referrer的值是http://www.hacker.com/index.html(以首页举例),所以后端只要通过Referrrer做白名单判断就能防这种常见的CSRF攻击。下面探讨几个容易被忽悠的问题。

1.1 Referrer 会不会被伪造或者篡改?

  • 在浏览器环境下,Referrer 是浏览器自己带上的,js 是改不了 Rerferrer,所以是不能被伪造和篡改的。

  • 浏览器插件能改 Referrer 么?答案:能改,但是浏览器插件攻击不属于 CSRF 攻击范畴,如果用户浏览器都已经被安装了黑客插件了就有更方便的攻击方法,但是不可能在所有用户浏览器都安装上黑客的插件。

  • 通过网关或者抓包修改 Referrer?答案:能改,这是中间人攻击,也不属于 CSRF 攻击范畴。防中间人攻击用 HTTPS。

  • 黑客通过自己后台代理,请求发到黑客自己的后台,黑客后台修改 Referrer 再转发到相册后台,可以改么?可行么?答案:能改,但不可行,请求发送到黑客自己后台不会带上相册的 Cookie,登录态校验通不过,敏感操作做不了。

1.2 用 Referrer 防 CSRF 安全么?

处理了下面几种特殊情况,用 Referrer 防 CSRF 是安全

  • 读操作不能有上面提到提到的两种特殊情况,不能用 JSONP,CORS 要白名单,所以读操作是安全的。

  • 写操作 Referrer 为空的时候不能放过,使用白名单机制,Referrer 在白名单内才放过。什么时候会为空?

    • 地址栏直接输入 url 的时候,第一个请求 Referrer 为空,一般是 html 页面,这种读操作不用防 CSRF

    • 使用 Referrer-Policy 策略设置 no-referrer 是,Referrer 为空,自己的页面不要这样设置,为了防黑客的页面设置了,所以为空的时候不能放过。

    • 还有一些 iframe 的特殊使用(以前用来绕过图片防盗链的)也会导致 Referrer 为空,这些情况都不能放过过。

写操作不能用 Get,如果写操作可以用 Get,由于 Img 标签,A 标签能发 Get 请求。所以在一些 UGC 网站,比如用户写日志可以插入自定义图片,能插入自定义连接,图片 img 标签 src 或者 A 标签的 href 就指向写操作的 URL,这样只要打开这篇日志就会发送这个 Get 请求,或者点击了日志上的连接,就帮用户做了写操作。并且 Referrer 还是合法的。这就是一种少见的 CSRF 攻击过程,其实也是最早期的 CSRF 攻击。这种攻击一旦成功,很方便做成蠕虫病毒,危害性极大。_PS 有人觉得这种少见的攻击过程不算 CSRF,应该算 XSS,好像也有点道理,但是常规的防 XSS 的方法貌似不好防这种特殊情况,下面要讲的 CSRF 的 Token 的防护方式是能防这种特殊情况的。

1.3 用 Origin 可以么?

可以,原理跟 Referrer 一样,Origin 请求头是 XHR2.0 里增加的,含义是发送请求页面的域名,主要目的是解决跨域问题。如果用来校验 CSRF 请求,就有一些细节要处理好,后台判断 Origin 时也要使用白名单,并且不能为空,不在白名单内的请求都直接返回失败,不能执行请求里的写操作(有一些 web server 是请求执行了,也返回了数据,只是没有配 ACAO 响应头,浏览器收不到,这种情况能限制跨域请求,但是不能防 CSRF 的写操作)。另外一种做法就是自定义 HTTP 请求头,把 HTTP 请求升级为复杂请求,这样在跨域的情况就会先发一个 Option 预检请求,预检请求通不过也就不会执行后面真实请求了。

1.4 既然能做到安全,为什么现在很少见用这种方案?

因为有更简单的方案,就是下面要讲的 Token 方案。

2、Token

上面讲到 Cookie 的一些特性的第二条,读写 Cookie 有跨域限制(作用域,Domain,Path),所以我们可以用这个特性来区分是自己页面还是黑客页面。只要页面能读(或者写)www.photo.com域名 Cookie,就证明是自己的页面。懂了原理,方案就很简单,比如服务器通过 cookie 下发一个 token,token 值是随机数,页面发请求的时候从 cookie 取出 token 通过 HTTP 请求参数传给后台,后台比对参数里的 token 和 cookie 里的 token 是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。防 CSRF 的方案就是这么简单,这种方法能够 100%防 CSRF,但是可能会有几个变种,下面探讨几种情况。

2.1 Token 是前台生产还是后台生产?

我上面举例例子是后台生成传到前台的,大家发现其实后台并没有存这个 token,所以原理上前后台生成都可以,只要保证随机性。如果前端生成 token 然后写到 Cookie 里,然后 HTTP 请求参数也带上 token,后端逻辑一样比对参数里的 token 和 cookie 里的 token 是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。这就是 Cookie 读和写的差别,只要能读写自己域名的 Cookie 就是自己页面。

2.2 推荐的最佳实践方案

由于登录态已经下发了一个登录态 key,防 CSRF 的 token 就复用这个 key,由于登录态 key 比较重要,尽量少明文暴露,所以前端拿到 key 后做了一次 Hash 放到 http 请求参数里,后端通过同样的 Hash 算法对 Cookie 里的 key 做 Hash 后跟参数里的 token 比对是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。这里对 Hash 算法要求不高,简单高效就可以。

2.3 Token 放在 HTTP 参数里的哪里?

放在 URL 的 querystring 里,Post 请求的 Data 里或者 HTTP 请求头里,这三种方式都可以,只是有一点点细微的差别,如果 querystring 里,可能会影响 Get 请求的缓存效果,因为重新登录之后 token 会变,url 也就变了,之前的缓存就失效了。如果放在 HTTP 请求头里,就需要使用 fetch 或者 XHR 发请求,这样会变成复杂请求,跨域时需要多一次 Option 预检请求,对性能多少有一点点影响。

2.4 用 Token 方案后写操作可以用 Get 么?

可以,从安全角度考虑是可以的,用了 Token 之后,Get 和 Post 的安全等级是一样的,上面讨论的那种少见的 CSRF 攻击过程也攻击不到了。但是从语义化考虑建议 Get 是还是处理读操作方便理解。

2.5 用 Token 方案后读操作可以用 JSONP 跨域么?

可以,可以使用 JSONP 跨域了,另外如果使用 CORS 处理跨域,建议还是需要对请求头的 Origin 做白名单限制,防止不同子域名相互影响。

2.6 如果页面有 XSS 漏洞,黑客拿到 Cookie 怎么办?

这个方法防不了 XSS,防 XSS 需要其他方法,比如 CSP,用户输入/输出做转义等。

3、是否还有其他方案

3.1 Cookie 的 SameSite 属性可以么?

不好用,SameSite 设计的目的貌似就是防 CSRF,但是我觉得不好用,SameSite 有三个值 Strict/Lax/None,设置的太严格,会影响自己业务的体验,设置的太松没有效果,就算最严格 Strict 模式,也防不了我上面提到写操作用 Get 请求,UGC 页面有自定义照片的情况。并且还有小部分老浏览器不支持,最终其实还是 Token 方案好用。

3.2 Cookie 的 HTTPOnly 属性可以么?

不行,HTTPOnly 表示这个 Cookie 只能是 HTTP 请求可以读写,js 没有读写权限,浏览器还是会默认带上,所以登录态校验是通过的。如果设置了 HTTPOnly 还有副作用,上面说的 Token 方案就用不了了。

3.3 验证码可以么?

不行,验证码是用来防机器暴力攻击的,验证码是用来确认敏感操作是自然人发送还是机器自动发送。这里举个图片验证码的例子,大概原理是前端通过 img 标签展示图片验证码给用户看(图片字母经过噪音处理的),这个图片 HTTP 请求也会设置一个 cookie 如 codeID=xxx(加密的),用户在输入框输入图片中展示的字母,敏感操作的 HTTP 请求通过参数把用户输入的 code 传给后台,后台拿到用户输入的 code 和 cookie 里的 codeID(通常需要通过 id 查数据库)做比较,如果一致就通过。这种验证码系统能够防机器攻击,但是防不了 CSRF,黑客同样可以在黑客的页面展示验证码给用户,通过诱导用户输入验证码完成攻击操作,只能是提高了 CSRF 攻击成功的门槛,但是只要黑客页面诱导信息劲爆还是有很大部分用户会上当的。因为用户不知道输入验证码后会产生什么影响。

验证码我在一些资料上看到说可以用来防 CSRF,我个人觉得是不行,包括手机验证码都不行,详细情况大家可以研究各种验证码的实现原理。我猜测有些人可能有不同意见,但是如果非要构造一种能防 CSRF 的验证码技术上也是可行的。我这里推演一下防护过程。就拿我上面举得验证码举例。验证码图片的 url 是固定格式的http://code.photo.com/codeImg.jpg?v=123。v是随机数,换一张时防止缓存用的。验证码每次请求会种一个Cookie,codeID=xxx,后台会存储这个codeID对应的真实code,用户输入图片看到验证码,要校验验证码的请求参数会带上用户输入的code,后台拿到参数code和Cookie里的CodeID查数据库后对比来判断是否输入正确。攻击方式,黑客可以在黑客页面用img标签展示这个验证码,因为验证码url是固定格式的,后面的流程是一样的。你可能的改进方案:

  • 验证码图片做防盗链。黑客破解方案:可以用 Referrer-Policy 的 no-referrer

  • no-referrer 不给通过。黑客破解方案:可以用 iframe 嵌入你自己的页面,只把你自己页面种的验证码区域展示出来

  • 我的页面不给 iframe 嵌入。黑客说,你成功的防住了

所以需要对验证码做(1)(2)(3)的改造才能防 CSRF 攻击。其实加的(1)(2)(3)措施都不是验证码的本意,验证码是本意用来的防机器攻击的,不加(1)(2)(3)措施也一样可以防机器攻击。这里就有三种观点了。观点一:我只要找到一个反例,找到验证码不能防 CSRF 的一个例子,我就证明了验证码不能防 CSRF。观点二:我只要构造一个能防的例子,对验证码做一系列额外的改造来防 CSRF,我就证明了验证码能防 CSRF。观点三:验证码提高了攻击门槛,攻防就是魔高一丈道高一尺的过程,加上验证码更安全。我个人赞成观点一。读者们你们觉得呢?

3.4 HTTPS 可以么?

不行,HTTPS 是防中间人攻击的,不是防 CSRF 的

3.5 不用 Cookie 可以么?

可以。个人觉得非常不好用,这里讨论两种方案。

方案一:登录态 key 不放在 Cookie。所以 HTTP 请求也不会自动携带 key,也就不存在 CSRF 漏洞,也就不用防了。但是这种设计我个人觉得在一些大型复杂网站是非常棘手难搞的,因为涉及到新开页面,多个页面之间登录态需要同步(其中一个页面退出登录,登录另外账户,或者登录态过期续期等都需要同步给其他页面),跨页面通讯也有好多方案,如果你使用 localstorage 等本地缓存的话,关闭页面还要清理缓存,缓存满了要清理,浏览器兼容问题等。这种大型系统还会遇到其他的一系列问题,也会有一些列的解决方案,系统会比较复杂,最终还不如用 Cookie 方便。

方案二:登录态 key 放 Cookie。CSRF 的 Token 不放 Cookie,后台生成 Token 藏在 HTML 页面里,后台也存了这个 Token。HTTP 请求通过参数带上这个 Token,后台拿到参数里的 Token 跟自己后台存的 Token 做校验。这个方案也是在一些资料里看到的。但是这个方案也是相当复杂,比如这个方案需要处理好几个关键问题,这个 Token 是有用户属性,要跟用户绑定的。如果 Token 跟用户无关,那么黑客可以用自己账户的 Token 欺骗做水平攻击。另外也有新开页面,多个页面是同一个 Token 还是不同 Token?如果不同 Token,后台需要存一个 Token 列表,列表长度有最大值?另外还有如果其中一个页面退出登录,再登录另外账户。那么其他页面登录态是同步了,但是 Token 如何同步?同样你也会有一些列的解决方案。当你把这些问题都解决了,最后你发现既然 Token 有用户属性,那么就可以当登录态用,就不用 Cookie 的登录态 key 了,又回到了方案一。

你可能感兴趣的:(前端,web安全,javascript)