前端安全之 CSRF

1,262 阅读6分钟

CSRF

CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是攻击者借助受害者的 Cookie 骗取服务器信任,伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。

举个栗子(非真实情况),在风和日丽的某一天你照常打开知乎摸鱼看技术文章,这时弹出来一个小广告,你不小心点了进去,退出来之后突然发现自己的文章都被删了,蛤???我没点删除啊?这到底是个什么情况!

这就是跨站请求伪造,攻击者网站利用你已经登陆的状态,和被攻击网站的安全漏洞执行了预期之外的操作。

到底是什么样的安全漏洞呢?我们来看下去吧。

前言

在正文开始之前,我们先讨论一下浏览器的存储,目前浏览器的存储方式有 cookie、Web Storage、和 IndexDB。

其中 Web Storage 是 HTML5 中新增的本地存储的解决方案,包括 localStorage 和 sessionStorage。解决了 Cookie 存储的空间太小,每次请求都携带导致带来了巨大的性能浪费等缺点。本文假设你已经有相关浏览器存储的技术储备,如果你不了解,请点击关于Cookie、LocalStorage、sessionStorage 与 IndexedDB

我们会在 cookie 或者 localStorage 中存储用户的认证信息,而这两种方式会带来不同的安全问题。

Cookie

先讨论 cookie,我们登录之后,将服务端发送给我们的 token 存储在 cookie 中,在 cookie 没过期之前,浏览器会在每一个接口请求中都发送 Cookie(包括别的域名下),那么问题来了, 假设没有别的验证用户的方式,只凭借 Cookie,服务器会将所有携带包含用户验证信息的 cookie 的请求都认为是用户自己的操作。

也就是说,在你从知乎的页面跳转出去访问别的页面的时候,假设这个页面带有相关接口的恶意请求,再加上 cookie 里面的认证信息,这个接口就请求成功了。

举个栗子

这个是知乎个人主页请求的一个接口

这是请求头部携带的 Cookie

这是返回的数据

我们在一个新的标签页中访问一下这个接口

啊欧,接口请求成功了!

当然啦,现在大家都考虑到了这个问题,一些重要的操作,比如点赞、删除之类的操作还是要进一步校验的。

那怎么防范呢?

我们还是拿知乎来举例噢

据我观察,这是知乎点赞的接口

请求成功会返回这个数据

CSRF 防范

我们故技重施,在新标签页中访问接口

啊咧?怎么肥四!

控制台里报错 403,说用户访问被禁止了。

我们来看看这个接口请求头部是不是加了什么别的奇奇怪怪的东西。

我们可以看到,他添加了一个 x-xsrftoken 字段。

是的,这就是防范 CSRF 攻击的方法之一,攻击过程中,攻击者借助受害者的 Cookie 骗取服务器的信任,但并不能拿到 Cookie,也看不到 Cookie 的内容。而对于服务器返回的结果,由于浏览器同源策略的限制,攻击者也无法进行解析。因此,攻击者无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。

要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

这是常用的一个防范 CSRF 的方法,添加 token 验证。

还可以使用验证码,比如我们删除 qq 好友的时候,有时会让我们输入验证码,但是验证码并不是万能的,因为出于用户考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。

其次我们可以看到请求头部中有一个 referer 字段。

它记录了该 HTTP 请求的来源地址。通过 referer check,可以检查请求是否来自合法的“源”。

可以在服务端增加如下代码:

if (req.headers.referer !== 'https://www.zhihu.com/people/xu-dan-79-70') {
    res.write('csrf 攻击');
    return;
}

意外情况

做完以上工作不代表你的项目百分百可以防范 CSRF 了,<img> 这个狡猾的标签可不受同源策略的限制,可以跨域加载资源,你的项目中如果有上传图片的组件,一定要仔细检查是否携带了 token。

<el-form-item :label="$t('xxx')" prop="xxx">
   <el-upload
       ref="upload"
       :on-success="handleAvatarSuccess"
       :on-error="handleAvatarError"
       :before-upload="beforeAvatarUpload"
       :headers="uploadHeader"
       action="/api/xxx/uploadPicture"
  >
 </el-form-item>
...
uploadHeader() {
   return {
     Authorization: `Bearer ${this.$store.getters.accessToken}`
   }
}

localStorage

有朋友就问了,cookie 这么容易被 CSRF 攻击,那我用 localStorage可以吗?

欸,这位朋友说得好,localStorage 的天然优势就是没有 CSRF 攻击的风险,为什么呢?

因为 localStorage 不会被浏览器自动发送,它就相当于一个浏览器本地的小型数据库,只负责存储数据,如果需要发送用户身份验证信息,需要手动从 localStorage 中取出并添加到请求头部。

...
// axios 请求拦截器
service.interceptors.request.use(
  config => {
    // do something before request is sent
    config.headers['Authorization'] = `Bearer ${localStorage.getItem('accessToken')}`
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

但是由于 js 读取 LocalStorage 毫无阻碍,所以很容易引起另一种安全问题 XSS。

XSS

XSS,即 Cross Site Script,中译是跨站脚本攻击,是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。

XSS 攻击又分为 3 种类型,具体情况我们下篇文章再来讨论。

XSS 防范

对于 Cookie 来说,防范 XSS 攻击只需要设置 httpOnly 就可以禁止通过 js 脚本来读取 Cookie。对于 localStorage 来说只能进行非常充分的 XSS 检查。

XSS 对于 CSRF 来说更加难以防范,因为即使你能排查自己代码中 XSS 隐患,但是你的项目中引用的第三方库同样可能存在 XSS 漏洞。

在任何用户输入的地方,都要进行检查、过滤和转义,尤其在使用富文本的地方对 < > 一定要进行过滤和编码。

参考

浅说 XSS 和 CSRF

如何安全储存JWT之Cookie与Web Storage