实践理解 Web 安全 04 CSRF 跨站请求伪造02

CSRF 防护策略

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

之前讲的 CSRF 的两个特点:

  • CSRF(通常)发生在第三方域名
  • CSRF攻击者不能获取 Cookie 信息,只是冒用

针对这两点,我们可以专门制定防护策略:

  • 阻止不明外域的访问
    • 同源检测
    • SameSite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重 Cookie 验证

同源检测

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

浏览器的同源策略根据 Origin 请求头来判断是否同源。

同样的,我们也可以根据两个请求头字段(HTTP Headers)来判断请求来源(在服务端操作):

这两个是浏览器发起请求时(并不是所有请求)自动添加的禁止修改的请求头字段,不能由前端自定义内容,所以可以放心的判断请求来源。

Origin

介绍

  • Origin 请求头字段表示请求来自于哪个站点
  • 该字段仅包含站点的域名,不包含其他信息(path路径、query参数等):<scheme>://<host>[:<port>]
    • scheme:协议
    • host:服务器域名或IP地址
    • port:端口(可选)
  • 该字段自动包含在 CORSPOST 请求的 Header 中

还有几种情况的请求中不会包含 Origin:

  • IE10以下版本不支持 Origin
  • IE在同源发起的 POST 请求不包含 Origin
  • 302重定向非同源站点:在302重定向到非同源站点之后发起的请求 Origin 会被设置为 null,因为 Origin 可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的 URL,因此浏览器不想将 Origin 泄漏到新的服务器上。
  • 来源页面采用的协议为表示本地文件的 file 或者 data URL,Origin 为 null

IE 的特例:

MDN 说 IE 浏览器的同源策略和其他浏览器不同,例如未将端口号纳入到同源策略的检查中,也就是 IE 会认为 https://company.com:81https://company.com 属于同源,不受任何限制。

所以当这两个站点互相发送 GET 请求时,由于不属于跨域请求(CORS)也不是 POST 请求,所以不会携带 Origin。

**但是!**本人经过测试,尽管端口号不同,在支持 Origin 的 IE 版本中发送跨域请求,还是包含了 Origin。

所以这个特例大概已经被修复了吧,不过没有找到依据证明。

总结

Origin 只包含在 POST 请求 和 CORS 跨域请求中,在这个范围下还有一些特殊情况不包含 Origin:

  1. IE10以下版本的任意请求不包含 Origin
  2. IE 向本站发起的 POST 请求不包含 Origin
  3. 302重定向到非同源站点发起的二次请求,Origin 为 null
  4. 来源页面采用的协议为表示本地文件的 file 或者 data URL,Origin 为 null

示例

新建文件夹 csrf-origin,在其中添加 home.html 文件(命名home主要用来证明 Origin 不包含路径):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Origin</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
</head>
<body>
  <script>
    $.get('http://localhost:1000') // 向本站发送GET请求
    $.post('http://localhost:1000') // 向本站发送POST请求
    $.get('http://localhost:2000') // 跨域发送GET请求
    $.post('http://localhost:2000') // 跨域发送POST请求
  </script>
</body>
</html>

serve 启动两个 Web 服务:

# 开启 web 服务,监听端口号:1000
serve . --listen 1000

###### 新开一个命令行窗口 ######
# 开启 web 服务,监听端口号:2000,开启 CORS 允许跨域请求
serve . --listen 2000 --cors

使用浏览器访问 http://localhost:1000/home

1、IE10以下版本的任意请求不包含 Origin:

IE10以下不支持 CORS 跨域请求,可以通过 IE9/IE8下的 XDomainRequest 对象允许 AJAX 应用程序在满足一定条件的时候,直接发起安全的跨域请求。

对于使用 jquery 的 ajax,可以使用第三方兼容库:jquery-ajaxTransport-XDomainRequest,使用 IE 条件语法,在低于 IE9 以下版本时引入兼容库:

<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<!--[if lte IE 9]>
<script src="https://cdn.jsdelivr.net/npm/jquery-ajax-transport-xdomainrequest@1.0.4/jquery.xdomainrequest.min.js"></script>
<script type="text/javascript">
	jQuery.support.cors = true
</script>
<![endif]-->

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2、IE 向本站发起的 POST 请求不包含 Origin

在这里插入图片描述

3、302重定向到非同源站点发起的二次请求,Origin 为 null

可以使用上面开启的 HTTPS 服务,开启 CORS 并添加一个 302 重定向接口:

# 安装中间件
npm i cors

app.js

const express = require('express')
const cors = require('cors')
const app = express()

/* ...其它代码 */

// 开启 CORS
app.use(cors())

/* ...其它代码 */

// 重定向测试页面
app.post('/redirect', (req, res) => {
  // 重定向到非同源站点
  res.redirect('http://localhost:2000')
})

home.html增加请求 :

$.post('https://localhost:3000/redirect')

在这里插入图片描述

在这里插入图片描述

4、来源页面采用的协议为表示本地文件的 file 或者 data URL,Origin 为 null

file 协议就是以文件方式在本地双击 html 文件打开页面:

在这里插入图片描述

data URL 就是使用 DataURLs 格式加载文档(一般用于图片),例如:

<iframe src="data:text/html,<!DOCTYPE html><html lang='en'><head><script src='https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js'></script></head><body><script>$.post('http://localhost:2000')</script></body></html>"></iframe>

在这里插入图片描述

5、其它正常情况

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Referer

介绍

Referer 请求头包含了当前请求的来源页面的地址,即表示当前请求是通过此来源页面里的链接进入的或发起的请求。

服务端一般使用 Referer 请求头识别访问来源,以此进行统计分析(例如站长统计)、日志记录以及缓存优化等。

Referer 的正确英语拼写应该是 Referrer,由于早期 HTTP 规范的拼写错误,为了保持向下兼容就将错就错了。

参见 HTTP referer on Wikipedia (HTTP referer 在维基百科上的条目)来获取更详细的信息。

与 Origin 的区别

RefererOrigin 相似,主要区别是:

  • Origin 字段只包含主机,Referer 还包含涉及到用户隐私的 URL 路径和请求内容。
  • Origin 只包含在 本站 POST 请求和 CORS 请求中(上面讲的特殊情况除外),而 Referer 则包含在所有类型的请求中(下面讲的特殊情况 和 配置 Referrer Policy 除外)

在应对隐私问题方面,Origin 要更尊重用户的隐私。

不发送 Referer 的情况

以下三种情况下,Referer 不会被发送:

  • 来源页面采用的协议为表示本地文件的 file 或者 data URL
  • 当前请求页面采用的是非安全协议,而来源页面采用的安全协议(HTTPS)
  • IE7/8下通过 window.location.hrefwindow.open 跳转页面

第一种情况可以参考 Origin 中的示例。

第二种情况示例:

在这里插入图片描述

在这里插入图片描述

第三种情况可以在后台尝试获取 referer:

app.js

// 首页
app.get('/', (req, res) => {
  console.log('referer:',req.headers.referer)
  res.render('index.html', {
    user: req.session.user,
    account: {
      total
    }
  })
})

其它正常情况可以参考 Origin 的示例。

Referrer Policy

对 Referer 发送不做限制是不安全的,会造成用户敏感信息泄漏(例如带有敏感信息的重置密码的 URL,如果被收集,则存在密码被重置的危险)。

于是2014年,W3C的Web应用安全工作组发布了 Referrer Policy 草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。

注意:Referrer Policy 并没有延续 Referer 的拼写错误,设置 Referrer Policy 策略的指令都是正确的拼写:referrer

设置方式

设置 Referrer Policy 的方式有三种:

  1. 通过 CSP(内容安全策略) 设置,服务器配置 Content-Security-Policy响应头
  2. 页面头部使用 name 为 referrer 的 <meta> 元素
    • 例如:<meta name="referrer" content="origin">
  3. 单独设置 HTML 标签的策略
    1. 使用 <a><area><img><iframe><script><link>元素上的 referrerpolicy属性设置独立的请求策略
      • 例如:<a href="http://example.com" referrerpolicy="origin">
    2. 也可以在 <a><area><link>元素上将 rel属性设置为 noreferrer(仅支持这一个策略)
      • 例如:<a href="http://example.com" rel="noreferrer">
属性值
描述
no-referrer请求不包含 referer 请求头字段
no-referrer-when-downgrade浏览器默认设置,在同等安全级别的情况下(请求站点的安全级别不低于来源站点:HTTPS -> HTTPS、HTTP -> HTTPS),发送 referer。降级(downgrade)的情况不发送(HTTPS -> HTTP)
origin任何情况下,仅将 origin 作为值发送,例如https://example.com/page.html 会将 https://example.com作为 referer 发送
origin-when-cross-origin对于同源请求,发送完整的 URL,对于非同源请求仅发送 origin
same-origin对于同源请求,发送完整的 URL,对于非同源请求不发送 referer
strict-origin在同等安全级别的情况下,发送 origin 作为 referer,降级情况下不发送 referer。(类似 no-referrer-when-downgrade + origin)
strict-origin-when-cross-origin对于同源请求,发送完整的 URL;非同源请求,在同等安全级别的情况下,发送 origin 作为 referer,降级情况下不发送 referer。
unsafe-url无论同源请求还是非同源请求,都发送完整的 URL,但是会移除参数信息

根据 Referrer Policy 策略,我们可以将其设置成 same-origin,对于同源请求发送 URL,非同源不发送。

示例
Policy(策略)Document(来源)Navigation to(请求)Referrer(referer请求头)
no-referrerhttps://example.com/page.html任意主机或路径不发送
no-referrer-when-downgradehttps://example.com/page.htmlhttps://example.com/otherpage.htmlhttps://exmaple.com/page.html
no-referrer-when-downgradehttps://example.com/page.htmlhttps://mozilla.orghttps://example.com/page.html
no-referrer-when-downgradehttps://example.com/page.htmlhttp😕/example.org不发送
originhttps://example.com/page.html任意主机或路径https://exmaple.com
origin-when-cross-originhttps://example.com/page.htmlhttps://example.com/otherpage.htmlhttps://example.com/page.html
origin-when-cross-originhttps://example.com/page.htmlhttps://mozilla.orghttps://exmaple.com
origin-when-cross-originhttps://example.com/page.htmlhttp😕/example.com/page.htmlhttps://exmaple.com
same-originhttps://example.com/page.htmlhttps://example.com/otherpage.htmlhttps://example.com/page.html
same-originhttps://example.com/page.htmlhttps://mozilla.org不发送
strict-originhttps://example.com/page.htmlhttps://mozilla.orghttps://exmaple.com
strict-originhttps://example.com/page.htmlhttp😕/example.org不发送
strict-originhttp😕/example.com/page.html任意主机或路径http://example.com/
strict-origin-when-cross-originhttps://example.com/page.htmlhttps://example.com/otherpage.htmlhttps://example.com/page.html
strict-origin-when-cross-originhttps://example.com/page.htmlhttps://mozilla.orghttps://exmaple.com
strict-origin-when-cross-originhttps://example.com/page.htmlhttp😕/example.org不发送
unsafe-urlhttps://example.com/page.html?query=test任意主机或路径https://example.com/page.html

如何阻止外域请求

服务端可以通过获取 Request 的 Header 请求头字段 Origin 和 Referer 来判断请求来源是否是本站域名、子域名或授权的第三方域名,以及其它未知域名。

无法确认来源域名的情况

我们知道在某些情况下请求不会携带 Origin 和 Referer。

攻击者还可以通过设置 no-referrer 在自己的请求中隐藏 referer,例如:

<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer"> 

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

可以确认来源域名的情况

对于不可信的域名,也不能全部直接阻止。

因为这个请求可能是网页请求(正常页面跳转),例如搜索引擎的链接跳转过来的,也会被当作外域请求,所以在判断的时候需要过滤掉页面的请求情况,通常要符合以下情况:

Accpet: text/html
Method: GET

但相应的,页面请求就暴露在了 CSRF 的攻击范围中,如果页面的 GET 请求可以对用户进行什么操作,防范就失效了。

例如,一个银行的首页,可以通过URL参数进行转账操作:

GET https://bank.example.com/action=transfer&money=1000&account=xxxxxx

严格来说,不应该将敏感操作交给这类请求,但仍有很多网站经常在页面 GET 请求上挂参数实现产品功能,这就存在 CSRF 风险。

这种请求在同源的情况下一样存在风险,例如可以在本域发布UGC(用户原创内容,例如评论),那么可以可以直接在本域发起攻击,无法使用同源检测策略进行防护。

总结

尽管 Origin 和 Referer 是禁止修改的请求头字段,但也还有很多伪造方法可以实现自定义,例如 使用CURL发送请求。

$ curl --referer http://www.example.com http://www.example.com

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

SameSite Cookie

为了从源头上解决 CSRF 攻击冒用 Cookie 的问题,Google 起草了一份草案来改进 HTTP 协议。

那就是为 Set-Cookie 响应头新增 SameSite 属性,设置是否允许跨站请求携带 Cookie,参考前面 Cookie 携带 的部分。

应该如何使用 SameSite Cookie

如果 SameSite 被设置为 Strict,浏览器在任何跨域请求中都不会携带 Cookie,包括从其它站点跳转过来,或从地址栏输入网址打开,CSRF 攻击基本没有机会。

如果跳转子域名 或者 新标签重新打开登录的网站,之前的 Cookie 都不会存在,就需要重新登录,对于用户来讲,体验不会很好。

如果 SameSite 被设置为 Lax,从其它页面跳转过来的时候可以使用 Cookie,可以保障外域链接打开页面时用户的登录状态。

但相应的,其安全性比 Strict 低,而且仍不支持子域名

如果网站有多个子域名,不能使用在主域名登录存储的Cookie,访问子域名还要重新登录。

总之,SameSite 是一个可能替代同源检测的方案,但目前还不成熟,其应用场景有待观望。

非前后端分离的应用,用 Cookie 存储用户凭证的比较多,前后端分离的应用,一般在请求头使用 token 防止 CSRF。

CSRF Token

同源检测 和 SameSite Cookie 在阻止大多数外域发起的请求。

而 CSRF 的另一个特征是无法直接窃取用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。

CSRF 之所有能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。

那么我们可以要求所有的用户请求都携带一个 CSRF 攻击者无法获取的身份标识。

服务器通过校验请求是否携带正确的身份标识,来区分正常的请求和攻击的请求,以区分 CSRF 攻击。

这个身份标识一般是服务器生成的一串字符串,作为客户端进行请求的一个身份认证的令牌,也就是 Token

原理

CSRF Token 的防护策略分为几个步骤:

  1. 发送请求生成 Token
    • 用户向服务器发送请求获取 Token,例如登录请求向服务器发送用户名和密码,服务器验证通过后,为这个用户生成一个 Token,并在当前会话(session)里面保存相关数据,比如Token、用户角色、登录时间等。
    • 该 Token 通过加密算法对数据进行加密,一般 Token 包含随机的字符串和时间戳的组合。
  2. 存储 Token 到客户端
    • 服务器将 Token 发送给客户端,以 Data 或 Cookie 方式,客户端存储到后存储到本地,例如浏览器的 Cookie、sessionStorage、localStorage 等。
  3. 发送请求时附加 Token
    • 发送请求时将 Token 以 query(GET请求的URL 参数)、body(POST请求的入参)、header(请求头)等需要手动操作的方式附加上,保证是在本站以正确的方式获取的 Token。
  4. 服务器验证 Token
    • 用户从客户端得到 Token,再次提交给服务器的时候,服务器需要判断 Token 的有效性,验证过程是先解密,对比加密字符串以及时间戳,如果加密字符串与 Session 中的一致且未过期,那么这个 Token 就是有效的。

这种方法比之前检查 Origin 或 Referer 要安全一些,Token 可以存放于 Session 中,每次接收到请求时将 Token 从 Session 中拿出,与请求中的 Token 进行对比。

如何使用

这种方法关键在于如何把 Token 以参数的形式加入请求。

首先不能从 Cookie 中获取,因为 CSRF 会默认冒用 Cookie。

其次没有必要对大量安全性要求不高、无敏感操作的请求添加 Token 校验的环节。

例如页面跳转要添加 Token,就要在服务端渲染页面的时候遍历 DOM 中的 a 和 form 标签,在地址上或表单参数上添加 token,如果是纯静态页面,就要前端用 JS 来处理,意义不大。

所以一般都是需要用户身份进行的操作请求才会进行 Token 校验。

另外验证码和密码也可以起到 CSRF Token 的作用,而且更安全,它们都是额外附加一个服务器生成的认证标识。

所以很多银行要求已经登陆的用户在转账时再次输入密码。

总结

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

但是此方法不适用于所有请求(浪费资源),且服务器使用 Session 存储、读取和验证 Token 会引起比较大的复杂性和性能问题。

建议仅对需要用户身份进行的敏感请求使用 Token。

双重 Cookie 验证

介绍

还有一种防御措施是使用双重提交 Cookie,利用 CSRF 不能获取到用户 Cookie 的特点,我们可以在 Ajax 和 表单请求是手动附加一个 Cookie 的值。

流程:

  • 用户访问网站,服务器向客户端发送一个 Cookie,内容为随机字符串,不用存储到 Session
  • 在前端像后端发起请求时,取出 Cookie,并添加到请求参数中
  • 后端接口验证 Cookie 中的字段与参数中的字段是否一致,不一致则拒绝。

这种方法相比 Token 简单许多,后端只需要对比请求中的字段,不需要存储、查询 Token

当然,此方法并没有大规模应用,它在大型网站上的安全性还是没有 CSRF Token 高,因为有太多请求可以造成 Cookie 的不准确:

  • 跨域请求,前端拿不到请求站点的 Cookie,无法完成双重 Cookie 认证
  • 网站存在 XSS 漏洞,攻击者可以修改网站的 Cookie,然后发送请求时携带自己配置的 Cookie 作为参数,对 XSS 受害用户再次发起 CSRF 攻击。

总结

双重 Cookie 认证的优点:

  • 认证标识存储于客户端中,减少服务端存储和操作压力,实施成本低

缺点:

  • 如果有其它漏洞(例如 XSS),攻击者可以注入 Cookie,防御失效
  • 跨域无法获取 Cookie,无法实施(子域名除外)
  • 为了确保 Cookie 传输安全,采用这种防御方式,最好确保用整站 HTTPS 的方式,否则也会有风险。

防止网站被利用

前面所说的,都是被攻击的网站如何做好防护。而非防止攻击的发生,CSRF 的攻击可以来自:

  • 攻击者自己的网站
  • 有文件上传漏洞的网站,例如攻击者上传 HTML 文件,然后再去访问,浏览器就会运行里面的代码
  • 第三方论坛等用户内容
  • 被攻击网站自己的评论功能等

对于来自黑客自己的网站,我们无法防护。但对其它情况,应如何防止自己的网站被利用称为攻击的源头呢?

  • 严格管理所有上传接口,防止任何预期之外的上传内容(例如 HTML)
  • 添加 Header X-Content-Type-Options: nosniff 用来禁用浏览器内容嗅探行为,对于style 和 script 文件,如果服务器端设置了不正确的 MIME 类型,那么请求将会被阻止
    • 互联网上的资源有各种类型,通常浏览器会根据响应头的Content-Type字段来分辨它们的类型。例如:"text/html"代表html文档,"image/png"是PNG图片,"text/css"是CSS样式文档。然而,有些资源的Content-Type是错的或者未定义。这时,某些浏览器会启用MIME-sniffing来猜测该资源的类型,解析内容并执行。
    • 更多参考内容嗅探攻击防护—X-Content-Type-Options
  • 对于用户上传的图片,进行转存或校验。不要直接使用用户填写的图片链接。
  • 当前用户打开其它用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)
    • 例如知乎、CSDN、掘金跳转到外域会先警告有风险。
  • 保证接口的幂等性
  • 接口不要在 GET 页面请求中做用户操作

总结

简单总结一下上文的防护策略:

  • CSRF 自动防御策略:同源检测(Origin 和 Referer 验证)和 SameSite Cookie
  • CSRF 主动防御措施:Token 验证 或者 双重 Cookie 验证
  • 对敏感操作的接口做更多的防护
  • 谨慎使用用户提交的内容
  • 对用户进行的有风险的操作进行提示

最佳的 CSRF 实践是解和上面讲到的防御措施方式中的优缺点来综合考虑,结合当前 Web 应用程序自身的情况做合适的选择。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值