概述
最近几年都做的是前后端分离中偏后端的项目,最近写了一点点前端的代码,用的是前后端分离的 restful 风格,但是 review 的时候被问起安全问题时,发现自己竟然没有考虑过这方面的问题,所以这里就来思考一下在前后端分离的 restful API 场景下,如何防止 CSRF 和 XSS 这两种常见的网络攻击。
CSRF 和 XSS
关于 CSRF 和 XSS 是什么,其他网络上资源也很多了,在我什么都不懂(其实现在也是什么都不懂)的时候,我觉得《白帽子讲 Web 安全》 这本书让我了解了很多的概念,此外,刚毕业的公司也让我知道了有这个东西的存在:OWASP Top 10 Web Application Security Risks。这里我就结合自己的理解介绍一样这两种攻击分别是什么东西,然后区别是啥,因为我一开始的时候也没理解他们的区别,觉得是同一个东西呀,后面体验测试了一下,就比较了解区别在于哪里了。
CSRF
CSRF 的全称是:Cross Site Request Forgery,中文叫跨站请求伪造,顾名思义,这种攻击就是跨越站点的一种攻击,攻击方式为你打开一个网站 A,但是网站 A 上放置了恶意代码,这个恶意代码会伪造你访问网站 B 的请求,如果你之前使用过网站 B,并且保持了网站 B 的登陆状态之类的,那么你在网站 B 的状态可能就会受到攻击。这也是我们经常说的,不要随便打开邮件中的不明图片和 HTML 代码,以及不要随便访问一些不良网站的一个重要原因。
XSS
和 CSRF 有些类似,但是 XSS 的攻击来源是你是正常访问一个正常的网站,但是,因为网站的设计者设计不合理,例如一个论坛没有对用户的输入进行校验,当你浏览论坛的帖子的时候,有一个恶意的论坛用户精心构造了一个可以修改论坛 HTML 代码的输入数据(在回帖子或者创建帖子的时候输入框输入的),然后论坛会将这个恶意用户的输入以帖子的形式展示出来,你刚好就访问到了这个帖子,那么这段被精心构造过的代码就被你的浏览器执行了,从而就让恶意用户的目的得逞了。
从这两个描述来看,这里首先,问题的来源就不一样,一个是你访问了不信任源,一个是你没有访问不信源,但是你访问的地方不够坚固,被攻击了,从而导致你受伤害;但是,这两个造成的后果有点类似,例如,都可以构造让你银行转账的请求,相比之下,XSS 比 CSRF 更难防一些。下面,我们就来聊一下,这两种攻击分别可以怎么来防止。
CSRF 的防御
先来聊一聊 CSRF 的防御,从前面的描述中,可以知道一般情况下,CSRF 的攻击是跨站点的,也就是从 A 访问 B,从这个特性出发,那么一些简单有效的操作方式就是禁止跨站点访问,实用操作有:
- B 网站对来源网站做一个白名单过滤,这个很常见,使用的原理就是从 A 网站访问 B 网站的时候,HTTP 协议是有 Origin 或者 Referer 字段支持的,通过这两个字段,我们在大多数情况下可以过滤非信任网站的请求。
白名单过滤并不总是有效,前面说了,大多数情况下有 Origin 和 Referer 字段,那么什么情况下没有这两个字段呢?这依赖于浏览器的实现,一般浏览器(流行的 chrome、firefix、safari、IE)来说,在以下几种情况下是不会有这两个字段的:
- 没有 Origin 字段
- IE11 同源策略:IE11 在跨站请求的时候不会有这个字段,但是 Referer 会有
- 302 重定向,因为这可能意味着跳转到其他的网站,可能存在一些隐私性,所以不带来源网站
- Referer 字段
- Referer 字段因为属于 HTTP 标准的字段,但是,因为标准也没有很严格,所以各个浏览器支持不一,所以也不一定会有
当没有这两个字段时,那是不是就没办法了?那倒不是,其实现在很多通用的框架都有提供 CSRF 的防御功能,他们的做法就是:
- 在网站 B 的表单上添加一个 CSRF_TOKEN,当你在网站 B 请求表单时,服务器就为你这个表单创建给一个 CSRF_TOKEN,并且放入表单中,当你提交表单时,同时会提交这个 CSRF_TOKEN,服务首先校验有没有 CSRF_TOKEN,CSRF_TOKEN 正不正确从而判断是不是 CSRF 攻击。
这个的思路在于如果你跨站的话,那么你是不能拿到我站点上一个动态的东西的:举例来说,你打开恶意网站 A,虽然恶意网站 A 可以向网站 B 发送请求,但是,因为我网站 B 对表单都有 CSRF_TOKEN 校验,你恶意网站 A 无法拿到这个 CSRF_TOKEN,那么就无从发起攻击。
但是,万事无绝对,如果遇到的是类似于 XSS 的同个网站的 CSRF 攻击的话,这个方法也可能会实现,举个例子,如果你的 gmail 收到一封邮件,这个邮件里面就包含针对 gmail 的恶意攻击脚本,你一旦查阅了这个邮件,触发了拉取恶意脚本,并且被浏览器执行了,那么恶意脚本模拟一个修改 gmail 配置的请求发给 gmail,因为这个脚本执行的上下文就在 gmail 上,所以它可以拿到 CSRF_TOKEN,从而跳过你的 CSRF 防御,这样你也被攻击了。
这个时候就是祭出最后的大招了,也是我们平时最烦的一招:验证码,所以平时在输入验证码或者再次输入密码确认的时候,心中的怨气小一些,因为这确实是为了你的安全考虑。
所以对于 restful 场景来说,如果纯粹的无状态,那么肯定是 CSRF 挂逼的,因此,如果我们在 WEB 场景下使用 restful,可以结合 WEB 的 cookie 特性,先进行一层 CSRF 的过滤,后面才进行 restful 的业务处理,总得来说:
- 网关接入层:来源网站+白名单过滤
- 中间层:CSRF token + 验证码过滤(CSRF token 可以通过 cookie 设置)
一个实际使用案例
图 1:一个实际的案例 |
---|
图片编辑链接 |
在这个案例中,Server 返回给浏览器的时候设置了一个 csrf_token
的 Cookie,然后前端收到之后,通过 JS 获取到 Cookie 里面 csrf_token
的值,然后再设置到请求的 Header
中,这样当 Server 收到一个 Post 请求的时候,Server 可以校验 Cookie 中的 csrf_token
的值是否与 Header
中的一致,从而判断这个请求是否是我们自己的 JS 请求的。
Origin 和 Reffeer header
Origin
和 Referer
Header 都能用于标识请求是从哪里来的,但是他们也是有一些区别的(废话,如果没区别还需要两个字段吗)。具体的区别在于:
- 内容不一样:
Origin
的内容格式是:<scheme>://<hostname>:<port>
,port 可能没有Referer
的内容格式是:URL
,甚至包括 URL param(https://example.com/page?q=123
)- 所以一个比较简单,一个比较完整
- 作用不一样:
Origin
的作用就是提供安全上下文;Referer
的作用主要是用于跟踪请求的路径,例如用于分析、记录和优化等等;
Origin
的设置时机Get
和Head
这两个 HTTP Method 不会设置Origin
(这个很丰富,表示跨站请求图片视频等都不会有)- 对于 302 重定向也不会设置
Origin
- 不设置
Origin
的情况下,Origin 的值可能是null
,即:Origin: null
Referer
的设置受另外一个 HeaderReferer-Policy
控制
Referer-Policy
- 取值
- 默认值
Referer-Policy: strict-origin-when-cross-origin
:- 当
Origin
一样时,发送完整的Referer
- 当
Origin
不一样时,并且协议相同(https->https)时只发送Origin
- 当
Origin
不一样时,并且协议降级,不发送Referer
- 当
Referer-Policy: no-referrer
:什么情况下都不设置Referer
Referer-Policy: no-referrer-when-downgrade
:当使用的协议相同或者更安全时设置Referer
,降级时不设置(HTTPS→HTTP, HTTPS→file
)Referer-Policy: origin
:Referer
只设置 origin 的值Referer-Policy: origin-when-cross-origin
:- 当
Origin
一样时,发送完整的Referer
- 当
Origin
不一样时,或者降级时,只发送Origin
- 当
Referer-Policy: same-origin
:跨Origin
时不发送Referer
headerReferer-Policy: strict-origin
:和no-referrer-when-downgrade
差不多,不过值只有Origin
unsafe-url
:无论何时,都设置完整的Referer
- 默认值
- 设置的方式
- HTTP Response 的 HTTP Header 中
- HTML 中
<meta name="referrer" content="origin" />
<a href="http://example.com" referrerpolicy="origin">…</a>
<a href="http://example.com" rel="noreferrer">…</a>
- CSS:CSS 也可以引用外部资源
- 通过
style
tag 的属性设置
- 通过
XSS 的防御
和 CSRF 不同,XSS 的主要风险来自于对用户输入的防御不足,恶意用户可以将通过精心构造的恶意输入提交到系统,并且被系统以错误的形式展示出来,从而导致其他正常访问系统的客户受到攻击。要说容易防御倒也简单,无非就是对用户的输入进行校验嘛,但是,这个输入其实是广泛的,平时要做到全面是一项非常困难的事情,需要全面的考虑和测试。
XSS 不仅仅是简单的表单文本输入可能存在攻击点,文件上传往往也是一个重要的入口,例如你的后台是 PHP 写的,如果你没有处理好用户上传 php 文件的话,很可能用户上传了一个 PHP 文件,然后下次被访问的时候就变成了执行这个 PHP 文件,导致服务器的攻击,这个更严重。
所以,在 REST 场景下,针对 XSS 的攻击,主要难点还是在于:
- 对用户的输入进行格式化处理,确保不会存在攻击的特殊字符;
- 对用户输入内容的展示需要额外的注意,确保特殊字符都是被转义过的。