素材巴巴 > 程序开发 >

Web 安全之 CSRF

程序开发 2023-09-11 18:19:47

什么是 CSRF?

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的 CSRF 攻击有着如下的流程。

  1. 受害者登录 a.com,并保留了登录凭证(Cookie)
  2. 攻击者引诱受害者访问了 b.com
  3. b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带 a.com 的 Cookie
  4. a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
  5. a.com 以受害者的名义执行了 act=xx
  6. 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让 a.com 执行了自己定义的操作

几种常见的攻击类型?


 

受害者在访问含有这个 img 的页面后,浏览器会自动发出一次 HTTP 请求。网站就会收到包含受害者登录信息的一次跨域请求。

这种类型的 CSRF 通常使用的是一个自动提交的表单。

在访问该页面后,表单会自动提交,相当于模拟用户完成了一次 POST 操作。

POST 类型的攻击通常比 GET 类型要求更加严格,但仍并不复杂。任何个人网站、博客、被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许 POST 请求上。

链接类型的 CSRF 并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户主动点击链接才会触发。

这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击。

母猪上树的视频...
 
 

由于之前用户登录了信任的网站 A,并且保存了登录状态,只要用户主动访问了上面的页面,则攻击成功。

CSRF 的特点?

CSRF 的防护策略?

CSRF 通常从第三方网站发起,被攻击的网站无法防止攻击的发生,只能通过增强网站自身针对 CSRF 的防护能力来提升安全性。

同源检测

既然 CSRF 大多来自于第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。

如何判断请求是否来自于外域?

在 HTTP 协议中,每一个异步请求都会携带两个 Header,用于标记来源域名。

这两个 Header 在浏览器发起请求时,一般都会自动带上,且不能由前端自定义内容。服务端可以通过解析这两个 Header 中的域名来确定请求的来源域。

使用 Origin Header 确定来源域名

在部分与 CSRF 有关的请求中,请求的 Header 中会携带 Origin 字段。字段内包含了请求的域名(不包含 path 及 query)。

如果 Origin 存在,那么直接使用 Origin 中的字段来确认来源域名。

但是 Origin 在以下两种情况下并不存在。

使用 Referer Header 确定来源域名

根据 HTTP 协议,在 HTTP 头中有一个字段 Referer,记录了该 HTTP 请求的来源地址。

对于 Ajax 请求、图片和 Script 等资源请求,Referer 为发起请求的页面地址。

对于页面跳转,Referer 为打开页面历史记录的前一个页面地址。

因此我们使用 Referer 中链接的 Origin 部分可以就得知请求的来源域名。

但这种方法并非万无一失,因为 Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有所差别,并不能保证浏览器自身没有安全漏洞。

使用验证 Referer 值的方法,就是把安全性都交托依赖于第三方(即浏览器)来保障,从理论上来讲并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的 Referer。

2014 年,W3C 的 Web 应用安全工作组发布了 Referrer Policy 草案,针对浏览器该如何发送 Referer 做了详细的规定。

新版的 Referrer Policy 规定了五种 Referer 策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin 和 Unsafe URL。而之前就存在的三种策略:never、default 和 always,在新标准里换了个名称,它们之间的对应关系如下表所示。

在这里插入图片描述

根据上面的表格,因此需要把 Referrer Policy 的策略设置成 same-origin,对于同源的链接和引用,会发送 Referer,Referer 值为 Host 不带 Path;跨域访问则不携带 Referer(例如:aaa.com 引用 bbb.com 的资源,不会发送 Referer)。

设置 Referrer Policy 的方法有三种。

注意:攻击者可以在自己的请求中隐藏 Referer。

 
 

此外,在以下情况下 Referer 可能没有或者不可信。

无法确认来源域名?

当 Origin 和 Referer 头文件都不存在时,建议直接进行阻止,特别是如果没有使用随机 CSRF Token 作为第二次校验时。

如何阻止外域请求?

通过 Header 的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域或者是子域名,亦或者是有授权的第三方域名,也可能是来自不可信的未知域名。

我们已经知道了请求域名是否是来自于不可信的域名,直接阻止掉这些请求,就能防御 CSRF 攻击了吗?

当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似 CSRF 攻击。

所以在判断的时候需要过滤掉页面请求的情况,通常 Header 需符合以下情况。

Accept: text/html
 Method: GET
 

但相应的,页面请求就暴露在了 CSRF 的攻击范围中。如果网站在页面的 GET 请求中对当前用户做了什么操作,防范就失效了。

GET https://example.com/addComment?comment=XXX&dest=orderId
 

注意:严格来说这种请求并不一定存在 CSRF 攻击的风险,但仍然有很多网站经常把主文档 GET 请求挂上参数来实现产品功能,这样做对于自身来说是存在安全风险的。

此外,CSRF 大多数情况下来自于第三方域名,但也并不能排除本域发起。如果攻击者有权限在本域内发布评论(含链接、图片等,统称 UGC),那么它就可以直接在本域发起攻击,这种情况下同源策略无法起到防护的作用。

综上所述,同源验证是一个相对简单的防范方法,能够防范绝大多数的 CSRF 攻击。但这并不是万无一失的,对于安全要求较高,或者有较多用户输入内容的网站,我们就需要对关键的接口做额外的防护措施。

CSRF Token

我们知道,CSRF 的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie、Header、网站内容等),仅仅只是冒用 Cookie 中的信息。而 CSRF 攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。

那么我们可以要求所有的用户请求都必须携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带了正确的 Token,来把正常的请求和攻击的请求区分开,也可以防范 CSRF 攻击。

CSRF Token 的防护策略可以分为以下三个步骤。

  1. 将 CSRF Token 输出到页面中

首先,在用户打开页面的时候,服务器需要给这个用户生成一个 Token,该 Token 通过加密算法对数据进行加密,一般 Token 都是包含随机字符串和时间戳的组合,显然在提交时 Token 就不能再放在 Cookie 中了,否则又会被攻击者冒用。因此,为了安全起见,Token 最好还是存放在服务器的 Session 中,在之后的每次页面加载时,使用 JS 遍历整个 DOM 树,对于 DOM 中所有的 a 和 form 标签后加入 Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 HTML 代码,这种方法就没有作用,还需要开发者在编码时手动添加 Token。

  1. 页面提交的请求携带这个 Token

对于 GET 请求,Token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。而对于 POST 请求,需要在 form 的最后加上 ,这样就可以把 Token 以参数的形式加入请求。

  1. 服务器验证 Token 是否正确

当用户从客户端得到了 Token,再次提交给服务器的时候,这时服务器需要判断 Token 的有效性,验证过程需先解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么表示这个 Token 是有效的。

这种方法要比检查 Referer 或者 Origin 安全一些,Token 可以产生并存放在 Session 中,然后在每次请求时把 Token 从 Session 中拿出,并与请求中携带的 Token 进行比对,但这种方法需要考虑如何把 Token 以参数的形式加入请求。

HttpServletRequest req = (HttpServletRequest)request; 
 HttpSession s = req.getSession(); // 从session中得到csrftoken属性
 String sToken = (String)s.getAttribute("csrftoken"); 
 if (sToken == null) { // 产生新的token放入session中sToken = generateToken(); s.setAttribute("csrftoken", sToken); chain.doFilter(request, response); 
 } else { // 从HTTP头中取得csrftoken String xhrToken = req.getHeader("csrftoken"); // 从请求参数中取得csrftoken String pToken = req.getParameter("csrftoken"); if (sToken != null && xhrToken != null && sToken.equals(xhrToken)) { chain.doFilter(request, response); } else if (sToken != null && pToken != null && sToken.equals(pToken)) { chain.doFilter(request, response); } else { request.getRequestDispatcher("error.jsp").forward(request, response); } 
 }
 

这个 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 与当前登录的用户进行比较,并将时间戳与当前时间进行比较。

小结

Token 是一个比较有效的 CSRF 防护方法,只要页面没有 XSS 漏洞泄露 Token,那么接口的 CSRF 攻击就无法成功。

但是此方法的实现比较复杂,需要给每一个页面都写入 Token(前端无法使用纯静态页面),每一个 form 及 Ajax 请求都必须携带这个 Token,后端对每一个接口都必须进行校验,并保证页面 Token 和请求 Token 的一致性。这就使得这个防护策略不能在通用的拦截上做统一的拦截处理,而需要在每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,而且有可能存在遗漏。

此外,验证码和密码其实也可以起到 CSRF Token 的作用,而且更安全。这也是为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码。

双重 Cookie 验证

在会话中存储 CSRF Token 比较繁琐,而且不能在通用的拦截上统一处理所有的接口。

另一种防御措施是使用双重提交 Cookie,利用 CSRF 攻击不能获取到用户 Cookie 的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。

  1. 在用户访问网站页面时,向请求域名注入一个 Cookie,内容为随机字符串(例如 csrfcookie=v8g9e4ksfhw)
  2. 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中(例如 https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)
  3. 后端接口验证 Cookie 中的字段是否与 URL 参数中的字段一致,若不一致则拒绝

此方法相对于 CSRF Token 简单了许多,可以直接通过前后端拦截的的方式自动化实现。后端校验也更加方便,只需要对比请求中的字段,而不需要再进行查询和存储 Token。

当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有 CSRF Token 高。

由于任何跨域都会导致前端无法获取 Cookie 中的字段(包括子域名之间),如果用户访问的网站为 www.a.com,而后端的 api 域名为api.a.com。那么在 www.a.com 下,前端拿不到 api.a.com 的 Cookie,也就无法完成双重 Cookie 认证。

于是这个认证 Cookie 必须被种在 a.com 下,这样每个子域都可以访问,任何一个子域都可以修改 a.com 下的 Cookie。

如果某个子域名存在漏洞被 XSS 攻击(例如 upload.a.com),虽然这个子域下并没有什么值得窃取的信息,但攻击者修改了 a.com 下的Cookie,攻击者就可以直接使用自己配置的 Cookie,对 XSS 中招的用户再向 www.a.com 下发起 CSRF 攻击。

小结

使用双重 Cookie 防御 CSRF 的优点。

使用双重 Cookie 防御 CSRF 的缺点。

为了确保 Cookie 传输的安全,采用这种防御方式时最好确保用整站 HTTPS 的方式,如果还没切 HTTPS 的,使用这种方式也会有风险。

Samesite Cookie 属性

为了从源头上解决 CSRF 的问题,Google 起草了一份草案来改进 HTTP 协议,为 Set-Cookie 响应头新增了 Samesite 属性,用来表明这个 Cookie 是个“同站 Cookie”,同站 Cookie 只能作为第一方 Cookie,而不能作为第三方 Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。(第一方 Cookie 指的是由网络用户访问的域所创建的 Cookie,第三方 Cookie 指的是建立在别的域名,不是你访问的域名即地址栏中的网址。)

即严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie。

假设 b.com 设置了如下 Cookie。

Set-Cookie: foo=1; Samesite=Strict
 Set-Cookie: bar=2; Samesite=Lax
 Set-Cookie: baz=3
 

用户在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但是 bar 会。

假设淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页,甚至是天猫页面的链接,点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接收到那个 Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。

即宽松模式,比 Strict 放宽了点限制:假设请求是改变了当前页面或者打开了新页面,并且同时是个 GET 请求,则这个 Cookie 可以作为第三方 Cookie。

假设 b.com 设置了如下 Cookie。

Set-Cookie: foo=1; Samesite=Strict
 Set-Cookie: bar=2; Samesite=Lax
 Set-Cookie: baz=3
 

当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,即用户在不同网站之间通过链接跳转不受影响。但如果这个请求是从 a.com 发起的对 b.com 的异步请求,或者是通过表单的 POST 提交触发的页面跳转,则 bar 也不会发送。

// 生成Token放到Cookie中并设置Cookie的Samesite
 private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {// 生成tokenString sToken = this.generateToken();// 手动添加Cookie实现支持"Samesite=strict"// Cookie添加双重验证String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI());httpResponse.addHeader("Set-Cookie", CookieSpec);httpResponse.setHeader(CSRF_TOKEN_NAME, token);
 }
 

如何使用 Samesite Cookie?

如果 Samesite Cookie 被设置为 Strict,那么浏览器在任何跨域请求中都不会携带 Cookie,新标签页重新打开也不会携带,所以 CSRF 攻击基本上没有机会。

但是跳转子域名或者是新标签页重新打开刚登陆的网站,之前的 Cookie 都不会存在。尤其是有登录的网站,那么重新打开一个标签页进入,或者跳转到子域名的网站,都需要重新登录。这对于用户而言,体验不是很友好。

如果 Samesite Cookie 被设置为 Lax,那么其他网站通过页面跳转过来的时候可以使用 Cookie,可以保障外域链接打开页面时,用户的登录状态仍然有效。但相应的,安全性也比较低。

防止网站被利用

CSRF 的攻击可以来自于以下场景。

对于来自攻击者自身的网站,我们无法防护。但对于其他情况,可以采取以下措施防止网站被利用成为攻击的源头。

总结

参考资料


标签:

素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。