CSRF - 前后端分离后带来的新问题

CSRF 的传统修复方式 https://blog.csdn.net/XiaoBeiTu/article/details/102268639

几天前,在阅读一篇极为专业的渗透测试报告时,发现了安全人员汇报了一个严重又常见的问题:CSRF 跨站请求伪造,诚然由于开发者的疏忽,产生 CSRF 的问题的确比较严重,好在发现的早我们可以尽早修复。安全人员是这样建议的:

The application should implement anti-CSRF tokens into all requests that perform actions which change the application state or which add/modify/delete content. An anti-CSRF token should be a long randomly generated value unique to each user so that attackers cannot easily brute-force it. It is important that anti-CSRF tokens are validated when user requests are handled by the application. The application should both verify that the token exists in the request, and also check that it matches the user’s current token. If either of these checks fails, the application should reject the request.

使用 anti-CSRF token 是防御 CSRF 的有效手段之一,安全人员的建议也很照本宣科的,很多 web 框架与编程语言都类似的实现方式,例如:

method="POST" action="/profile"> type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> ... >

或者使用 cookie / meta 与 ajax 全局设置,例如:

// 在 HTML 里面塞入这个 meta
="csrf-token" content="{{ csrf_token() }}">

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

**核心思路是,使用一个由服务器派发的 token,在前端进行状态修改时,也同时提交这个 token(往往会放在 html form 的 input 中,或者 ajax header 中),这时候服务端验证该 token 是否是之前所生成的,以此来判断这个请求是否被允许。**所以,当用户点击 hacker 所提供的三方网站时,这些恶意网站无论如何也无法获取到之前服务端生成 token,这样的话,即使请求可以发送至服务端,也不会通过验证。关于 CSRF 与 anti-CSRF Token 的具体机制本篇不再赘述,请参考这篇文章,此外,防范 CSRF 是一个稍微复杂的实践,还可以使用 referer、origin 等其他手段进行深度防御,而且一定是根据具体现状的考虑。

按照这位安全人员的建议,我们应该在渲染 form 时,嵌入 token 作为 hidden input 并且在后端进行验证,对于成熟的 web 框架来说,Spring MVC、Ruby on Rails、Play 或者 Lavarel 几乎是两行代码的事情,那么为什么做不到呢?

前后端分离带来了新问题

随着前后端分离与单页应用的到来,我们往往在后端使用 RESTful 的方式暴露接口,前端使用 react、angular 或者 VUE 来控制渲染和交互,那么,也就不存在如何在 form 中放入一个 token 来进行 CSRF 的验证了。对于 RESTful 的接口,本质上是无状态的(stateless),而 anti-CSRF token 是依靠 session 中的状态来进行判断,那么也就无法再使用这种方式了。

以前

前后端分离后

可以看到,在前后端进行分离后,最简单的集成方式如上图:
1)用户通过浏览器请求某个网站例如 www.google.com,然后 DNS 转移至前端站点,获取前端资源
2)返回页面,JS,CSS 等后,浏览器进行渲染页面,这时候用户就能看到页面了
3)在页面准备好后,用户的所有操作(不论是 form 提交、还是 ajax 请求),都发送给后端服务,再通过 web service 响应,修改页面,支持业务逻辑

这个流程中,对于真正存储、修改用户数据的后端服务,是无状态的,而用户所操作的 form 是完全由前端应用控制,后端服务无法感知。所以,即使前端使用某种方式在 form 中放入了 token,但是后端也无法验证,这种 anti CSRF token 的方式是无法实现的。

尝试引入状态进行修复

好消息是,自从单页应用的崛起我们已经很少直接使用 form 的方式跟后端服务打交道了(页面上也许有 form,但是提交走 ajax),通过 OWSAP CSRF Cheat Sheet 中的这一节 JavaScript Guidance for Auto-inclusion of CSRF tokens as an AJAX Request header,你依旧可以使用 token 的方式,具体的步骤是:

1)在某个地方存储 CSRF Token,推荐是 DOM,或者在 JS 变量中或者其他地方,不推荐 cookie 或者 localStorage。

name="csrf-token" content="{{ csrf_token() }}">
  • 1

2)在 ajax 中,使用自定义的 header 发送 CSRF Token。



> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3)在后端服务进行验证。

首先这种方式不能直接使用,并且也不是完全安全的,有这样几个问题:

1)存储 CSRF Token 的地方,无论是 DOM,Cookie 或者 localStorage,只要是 JavaScript 能读取到,就会面临 XSS 风险,很容易拆东墙补西墙。
2)很难在合适的时机放入 CSRF Token,还是单页应用的问题,获取完单页应用后,前端的渲染逻辑完全是浏览器负责,这是无法生成 CSRF Token 的。
3)就算前端代码在神奇的某处生成了 CSRF Token,后端应用也无法获取到 Token 用来验证请求是否合法,后端服务是无状态的。

解决这些问题的套路也不复杂,无非就是引入一个状态,也就是生成 token 与验证 token 的组件应该是一个,而且对于后端的服务来说,这是透明的。那么使用 API Gateway 或者自己写一个 Security Sidecar 就可以做到。大约是这样的逻辑:

再引入一个新的模块

看起来我们是解决了这个问题,我们引入了新的安全模块,它有可能是写在 WAF里,也有可能是 Security Sidecar 或者自定义的 API Gateway,总之,它在哪里用什么技术实现并不重要,重要的是这几个职责:

1)生成 CSRF Token 并且验证下来的请求
2)顺便可以做 token 验证,来确保用户是否有权限使用后端服务
3)常用的 HTTP Referer 与 Origin 检查
4)其他的安全拦截,比如基于 User-Agent 或者 IP 等等

使用 Origin/Referer Header 进行防范

不论你采用哪种方式实现了 CSRF Token,或者压根没做,但是通过 Origin/Referer 的验证判断是必须要做的,你可以参考以下的代码实现。听起来这种策略很完美,但是取决于浏览器的实现以及后端服务端支持的 HTTP Method(比如有程序员写的端口,通过 GET 方式去修改状态)。

        /* STEP 1: Verifying Same Origin with Standard Headers */
        //Try to get the source from the "Origin" header
        String source = httpReq.getHeader("Origin");
        if (this.isBlank(source)) {
            //If empty then fallback on "Referer" header
            source = httpReq.getHeader("Referer");
            //If this one is empty too then we trace the event and we block the request 
            //(recommendation of the article)...
            if (this.isBlank(source)) {
                accessDeniedReason = "CSRFValidationFilter: ORIGIN and REFERER request" + 
                "headers are both absent/empty so we block the request !";
                LOG.warn(accessDeniedReason);
                httpResp.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedReason);
                return;
            }
        }

        //Compare the source against the expected target origin
        URL sourceURL = new URL(source);
        if (!this.targetOrigin.getProtocol().equals(sourceURL.getProtocol()) || 
            !this.targetOrigin.getHost().equals(sourceURL.getHost())
        || this.targetOrigin.getPort() != sourceURL.getPort()) {
            //One the part do not match so we trace the event and we block the request
            accessDeniedReason = String.format("CSRFValidationFilter: Protocol/Host/Port " + 
            "do not fully matches so we block the request! (%s != %s) ",
                this.targetOrigin, sourceURL);
            LOG.warn(accessDeniedReason);
            httpResp.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedReason);
            return;
        }

请求首部字段 Origin 指示了请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息。该首部用于 CORS 请求或者 POST 请求。除了不包含路径信息,该字段与 Referer 首部字段相似。Referer 请求头包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。

使用 Samesite Cookie Attribute

一般认为是 CSRF 的终极解决方式,SameSite 是最新的 cookie 属性,如同 http-only 与 secure 一般,目前在 RFC6265 中推出,这个属性顾名思义,就是限制 cookie 只能在同站中使用。我认为是很好的,因为我一直是使用 cookie 进行系统的认证与授权设计,即使用 http-only,secure 确保只有浏览器能够获取 cookie,而 JS 不能,同时,通过 domain、path 与 expires 来控制 token。这样对前后端都非常友好,此外也很安全。对于 CSRF,除了 token 与 Origin/Refer 的方式,还可以使用其他更严格的做法。

目前 Samesite 的可选值为 Lax, Strict 或 None。对于 Strict 值,用来阻止浏览器在任何跨站的情况下发送 cookie,只有当前网页的 URL 与请求目标一致,才会带上,所以用户体验可能会遭受影响,特别是你的后端服务在不同的域下,具体请参考阮一峰的文章

很遗憾,如同下面引用的那句话一样,我们不得不信任浏览器的安全实现,这在网络时代是无法避免的。如同安全方法一样,能做到什么级别的安全取决于成本与投入,安全只是一种平衡,绝对的安全是不存在的。

At the end of the day you have to “trust” the client browser to safely store user’s data and protect the client-side of the session. If you don’t trust the client browser, then you should stop using the web at all for anything other than static content.

更严格的保护

某些时候我们需要更严格的保护,特别是一些安全级别很高的后台或者服务,可以考虑以下这几种方式

  • Re-Authentication (password or stronger):在进行安全级别较高的操作时,需要用户重新认证
  • One-time Token:使用类似于 HOTP / TOTP 的 token 进行认证
  • CAPTCHA:验证码其实也是一种选择

参考资料

前后端分离下的跨域问题以及CSRF攻击

 
转载

  前段时间新工程刚开始搞前后端分离,于是使用了一直被传的神乎其神的vue,在使用一段时间后,发现自己再也回不去以前用jquery操作dom的时代了。这个东西确实牛逼,大大简化了开发的工作量,将dom呈现从逻辑剥离出去,使开发人员更专注于注业务实现。但是前后端分离页因此带来了一系列问题,比如典型的跨域问题。于是趁着研究跨域解决方案的机会,我顺带着把session和token之间一直模糊的点也了解了个通透。

一、跨域(源)

  首先我们先来了解一下什么是跨域,当网站不支持跨域时会报如下错误。

  如下图所示,当我们打开一个网页时可以看到在请求头中有个origin字段,这个字段的值就代表着当前窗口所在的域,也叫做源,从字面意思我们就可以理解这个页面的来源。

  而返回头中有个access-control-allow-origin字段表示:你请求的服务端,能接受哪些域过来的请求,该值可以包含多个域,若该值为“*”表示接受任何域。

例如你打开了一个A站的页面,A站页面发送了一个对B站服务端的请求,这个请求头就会在origin字段中带上A站的域名,B站接收请求后将origin与自身后台设置的access-control-allow-origin字段对比,发现没有A站的域,那么根据浏览器同源策略,这个请求将会被拦截(请求还分为简单请求和非简单请求,原理一样,这里就不赘述了),并将返回头消息返回给浏览器。而前后端分离最直接的问题就是两端的域不一样,最简单粗暴的解决办法就是在后端过滤器中设置access-control-allow-origin字段的值,便可解决跨域问题。

二、Session

  基于session的校验方式,流程其实很简单,用户第一次登陆网站的时候,在服务端创立对应的session对象,该session对象通过sessionid来做唯一识别,并将该id返回给浏览器,浏览器拿到id后将其保存在cookies中。之后用户再次访问此域的页面时,浏览器可以随时从cookie中取sessionid,而不需要做额外的校验,只要在发送请求时再次提供id,服务器便能根据id在内存中找到对应用户的身份信息。

  但是session有两个弊端,第一个就是当用户数量越来越多,内存中保存的用户信息也越来越大,十分占据内存;第二个是若该域支持第三方网站发送请求,也就是支持任何其他网站发送跨域请求时,会产生csrf漏洞。

  所谓csrf漏洞其实很好理解,例如有一个支持跨域的银行网站,该网站有一个转账链接:http://xxxx.com?account=alice&count=1000&reciver=jack,当支持跨域时,这个链接可以嵌在任何页面,而且不会因为跨域问题被拦截。此时我们的主人公alice登陆了银行网站,此时session创建成功,并且将sessionid保存在cookies中,但这时候不小心打开了一个恶意网站,这个网站中嵌入了上述的转账链接并且触发了链接,然后浏览器会自动将携带cookie中的信息发送到后台做身份校验,后台校验通过,于是alice的1000块就这样转给jack。

三、Token

  于是我们在想,既然使用cookie中的sesssionID,有风险,那我们能不能不用cookie?于是token就产生了(当然token只是解决csrf攻击的手段之一)。在了解token之前,我们需要先简单了解一下加密算法,一般最常用的加密算法为sha256。该算法有一个密码,我们称之为密钥,用户信息+密钥在经过该算法计算后,会得出token。该算法原理其实和我们以前学过的微分方程有点像,每组三个解,其中有一个是公共解(通解),这个公共解就是密钥(所以sha256也是可以根据多组解计算之后破译的,因此我们需要经常更换密钥)。另外两个解,一个是用户的身份信息,另一个就是token了,后面再次请求后台时都需要带上这两个东西。即便用户在客户端擅自修改了自己的身份信息也不怕,因为修改后的身份信息传送给后台后,后台会再次用sha256计算一次,修改后的用户信息+密钥算出的token与原token不一致,请求不通过。

  token在服务端通过上述算法生成之后,连同其中的用户信息一起发送给浏览器,然后由浏览器将其保存在窗口内存中,每次点击转账链接时只需将token和自己的信息也一起发送给服务端(http://xxxx.com?account=alice&count=1000&reciver=jack&token=123456)这样即使恶意网站内嵌了转账链接触发了也不怕,因为恶意网站无法跨窗口拿到自己的token和用户信息(而之前使用session时,转账链接会自动将cookie中的sessionid发送给后台完成身份校验)。

参考资料:http://www.ruanyifeng.com/blog/2016/04/cors.html

参考资料:https://www.cnblogs.com/wxinyu/p/9154178.html

参考资料:https://www.jianshu.com/p/1ad4358beff7

posted @ 2021-07-25 13:03  charyGao  阅读(117)  评论(0)    收藏  举报