作者: TooooBug
链接:http://www.imooc.com/article/18069
来源:慕课网
XSS 全称 Cross Site Scripting ,跨站脚本攻击,因为 CSS 这名字老早就被样式表拿走了,所以只好叫 XSS 了。
XSS 是什么鬼
XSS 比如我只想在页面上显示一个名字:
<span class="name">{{name}}</span>
但是,如果我的名字是长这样的:
星辰<script>alert('SB')</script>
这时候就好玩了:
<span class="name">星辰<script>alert('SB')</script></span>
你看,页面中凭空多了一段脚本。
JS可以干嘛?用户登录用的JS吧,读取资料用的JS吧,点击买东西、消费用的JS吧,查用户有多少钱用的JS吧,基于 Cookies 也可以读写吧。
XSS 怎么防御
一个经典的防御方法就是对内容进行转义和过滤,比如
var escapeHtml = function(str) {
if(!str) return '';
str = str.replace(/&/g, '&');
str = str.replace(/</g, '<');
str = str.replace(/>/g, '>');
str = str.replace(/"/g, '&quto;');
str = str.replace(/'/g, ''');
// str = str.replace(/ /g, ' ');
return str;
};
var name = escapeHtml(`<script>alert('SB')</script>`);
此时 name 会变成
<script>alert('SB')</script>
这样就会原样显示出来,再也无法耍流氓啦。
当然,富文本还要更麻烦一些,因为要保留一部分标签和属性,要不然全变纯文本了,就不富了。这种情况一般通过黑名单进行过滤,或者白名单放行。即只允许一部分指定的标签和属性,其它的全部转义掉。
CSP 大法
前面转义的方法的出发点,是让用户的输入不要变成程序,输入的什么就让它输出成什么。
事实上现代浏览器为我们带来了一个全新的安全策略,叫作内容安全策略,Content Security Policy,简称CSP。CSP的思路跟转义不一样,它的着手点是,如果一段代码变成了程序,我们是否应该运行它。或者更准确一点说,它实际上是定义页面上哪一些内容是可被信任的,哪一些内容是不被信任的。
因为我们自己的脚本是预先就知道并放在页面上的,所以我们可以设置好信任关系,当有 XSS 脚本出现时,它并不在我们的信任列表中,因此可以阻止它运行。
它的具体使用方式是在 HTTP 头中输出 CSP 策略:
Content-Security-Policy: <policy-directive>; <policy-directive>
从语法上可以看到,一个头可以输出多个策略,每一个策略由一个指令和指令对应的值组成。指令可以理解为指定内容类型的,比如script-src
指令用于指定脚本,img-src
用于指定图片。值则主要是来源,比如某个指定的URL,或者self
表示同源,或者unsafe-inline
表示在页面上直接出现的脚本等。
详细的指令和值,可以查看MDN相关页面。
具体到上面的 XSS 例子,可以使用
Content-Security-Policy: script-src 'self';
这样除了在同一个域名下的JS文件外,其它的脚本都不可以执行了,自然之前 XSS 的内容也就失效啦。简单粗暴有没有?
当然,如果你说,我就是要在页面中放点内联的脚本,不可以么?当然可以啦,CSP 设计的时候也考虑了这些情况,还是相当灵活的。你只需要指定一个 nonce 属性,或者计算一下 hash 值,即可。详细的用法看 MDN 哦。
说实话,用 CSP 来处理 XSS 攻击还是不如转义来得优雅,因为转义可以不影响用户输入输出,不改变内容的本质。但是 CSP 提供了足够简单而又灵活的方式来防御 XSS ,可以很好地作为我们前端 XSS 防御的最后一道防线。
CSRF
CSRF 也是个望文生不到义的词,它的全称是 Cross Site Request Foggy,即跨站请求攻击。虽然也有跨站,但我觉得这个跨站还是相当可以理解的,它真的是从别的网站发起一个请求到我们的网站的。
当一个用户登录我们的网站后,在 Cookies 中会存放用户的身份凭证。在大部分时候,就是一个 SessionId 。当用户下次访问我们的网站的时候,我们用这个凭证识别出用户是谁,有没有登录态。
如果第三方网站的代码请求了我们的网站,会发生什么呢?比如
<img src="//www.example.com/haha" />
虽然它是一张图片,但它确实向www.example.com
发了一个请求,如此用户有登录态的话,其实就相当于是用户自己发了一个请求。如果这个地址是一个发表文章、发布微博甚至转账之类的链接,那用户就在不知情的情况下进行了一些操作。这也是比较严重的安全问题。
当然你可能会说,现在谁还这么弱智,把这么敏感的操作用 GET 啊?没错,你可以选择用 POST ,但是这丝毫不能阻止 CSRF 攻击的发生啊。
<iframe name="test"></iframe>
<form target="test" method="post" action="http://www.example.com/haha">
...
</form>
当这个表单提交的时候,我们就发了一个 POST 请求。华丽丽的 CSRF 。
CSRF 的常规防御
CSRF 比较常规的防御方式是通过判断来源和加 token。
判断来源比较简单,主要是判断referer
这个头,如果不是自己的网站,就返回错误。
加 token 即同样的随机 token,在 cookies 中放一份,在表单中再放一份。这样第三方网站就无法获取到这个 token 是什么。
但是这样做也有一个比较明显的问题,就是无法保证站内用户的体验。虽然你防了站外的攻击,但是也降低了站内用户的体验。具体表现在如果同时打开多个表单,只有最后一个表单能成功提交。
same-site 的 Cookie
回想 CSRF 之所以能够攻击成功,核心原因就在于用户的身份是放在 Cookies 中的,而不管你通过什么方式访问网站,都会带上这个网站的 Cookies ,从第三方来的访问自然也不能例外。
但是,Chrome 在这个问题上给了我们不同的答案,可以放第三方访问时不带 Cookies 。也就是说 Cookies 只有本站能用,来自第三方的访问都不能使用。
具体的使用方式,是在打 Cookie 的时候,加上一个属性:SameSite
,它的值有两:
-
strict
任何来自第三方的请求都不能使用 Cookies ,包括通过链接点进来的 -
lex
只有比较敏感的操作不带 Cookies ,比如表单提交
针对 CSRF ,我们可以将 Cookies 设置成SameSite: strict
的,这样就可以有效防御 CSRF 了。不过比较可惜的是,目前只有 Chrome 才支持这一属性。希望未来所有浏览器都能跟上脚步。
使用 SameSite 还会面临一个问题,如果用户是点击链接进来的,那么是不能使用登录态的。一般可以考虑将用户不敏感的信息不设置这个属性,点进来仍然可以显示当前用户是谁,但是在请求的时候要求一个比较敏感的 SameSite 的 Cookies。这里需要更多的实践经验来探索。
小结
本文重点讲了 XSS 和 CSRF 这两种比较常见的前端安全问题的防御思路,尤其是如何使用一些新的规范、实现来帮助我们进行防御。希望后面浏览器对这些安全相关防御办法的普及率能再高一些,让前端工程师能花更少的时间写出更安全的代码。