前端安全-如何防止XSS攻击
前端安全
随着互联网的高速发展,信息安全问题已经成为企业关注的焦点之一,而前端又是引发企业项目安全问题的高危据点。在移动互联网时代,前端人员除了受到传统的XSS、CSRF攻击之外,还时常遇到网络劫持,非法的Hybrid API 等新型的网络安全问题。当然,随着浏览器的不断发展和优化,不断引入了 CSP、Same-Site Cookies 等新的技术来解决问题。下面讨论一下前端人员如何防止XSS攻击。
XSS攻击的介绍
XSS(Cross-Site Scripting,跨域脚本攻击)攻击是最常见的 Web 攻击,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。其重点是『跨域』和『客户端执行』。
XSS攻击分类
- 反射型XSS:常见情况是攻击者通过构造一个恶意链接的形式,诱导用户传播和打开,由于链接内所携带的参数会回显于页面中或作为页面的处理数据源,最终造成XSS攻击。
- 存储型XSS:与前者不同,存储型XSS是持久化的XSS攻击方式,通过用户输入个人信息或者发表文章的方式将恶意代码存储于服务器端,当其他用户再次访问页面时触发,造成XSS攻击。
- DOM based型XSS:同样也是利用对数据源不可靠,缺乏过滤,由于开发过程中,不可避免部分数据需要回填到DOM中,例如href、src、data-id等标签属性,而通过构造特殊的字符串闭合原有的DOM标签,在其中注入script标签的方式造成攻击。
XSS的防范手段
针对XSS的防范,需要开发者养成意识,针对用户输入源的数据进行过滤,针对页面不可信任的数据源也要做好过滤,类似于响应头中的字段、url中的参数、refer等字段都是不可信的,并且结合其他手段和方法,让页面更加安全可控。
1. Htmlencode 转义特殊字符
在大部分的表单填写中,比如注册账号,会运行用户填写个人信息,包括昵称、邮箱、简介描述等,此类信息属于非富文本类型,最常用的处理方法是,对尖括号等特殊字符转义成实体字符进行存储,由于是非富文本信息,并且以标签内容形式展示,推荐使用 innerText 展示到页面中。
const htmlEncode = function (handleString){ return handleString .replace(/&/g,"&") .replace(/</g,"<") .replace(/>/g,">") .replace(/ /g," ") .replace(/\'/g,"'") .replace(/\"/g,"""); }
2. 引入XSS库针对用户输入源过滤,设置标签白名单
上面提到的都是非富文本的处理方法,然而在很多论坛、博客、商城中的页面排版,都是通过富文本的形式编辑的,即回显到页面中的就是一段HTML内容,不能再用纯文本的形式处理了。
大部分的富文本编辑器原理,都是提供一个具备contenteditable属性的dom元素,让用户对一段富文本进行编辑,其本质是对一段html进行处理,新增或删除样式等,最后通过回传富文本框中html的方式提供给开发者,意味着我们要允许用户填充一段html于我们的页面中。而获取到的html字符串我们不能直接进行简单的标签替换,否则会导致原有的样式丢失,最终展示在页面中的也不再是一篇排版精致的文章,因此我们要另寻他路。
无论是这份来自于富文本编辑器的html,还是来自于最终用户发起请求所获取到的html,都是不可信的,意味着在前端进行过滤是没有任何实际意义和价值的,因为攻击者可以轻易的伪造请求绕过限制,所以我们需要在我们的服务器端针对这段html进行过滤处理。针对html的标签白名单过滤,不同的语言有不同的库实现,这里主要介绍nodejs中常用的标签过滤库,nodejs中常用的库主要是xss和xss-filter,下面以xss库的使用为例:
// npm install xss const xss = require("xss"); function handleXss(content) { // 设置HTML过滤器的白名单 const options = { whiteList: { p: ['class', 'style'], em: ['class', 'style'], strong: ['class', 'style'], br: ['class', 'style'], u: ['class', 'style'], s: ['class', 'style'], blockquote: ['class', 'style'], li: ['class', 'style'], ol: ['class', 'style'], ul: ['class', 'style'], h1: ['class', 'style'], h2: ['class', 'style'], h3: ['class', 'style'], h4: ['class', 'style'], h5: ['class', 'style'], h6: ['class', 'style'], span: ['class', 'style'], div: ['class', 'style'], img: ['src', 'class', 'style', 'width'], }, }; // 自定义规则 const myxss = new xss.FilterXSS(options); // 直接调用 myxss.process() 即可 content= myxss.process(content); return content; }
通过限定白名单,仅允许常见的文本展示标签以及图片img标签进入白名单,这部分会再过滤后被保留,并且标签内的class和style属性也会被保留;其他属性和诸如script、iframe等标签都会被直接过滤掉。
const html = ` <p class="test" onclick="alert('xss');" ab="cd">123</p> <img src="/xxx.png" onerror="alert('xss')" /> <12a></12a> <script></script> `; console.log(handleXss(html)); /* <p class="test">123</p> <img src="/xxx.png" /> <12a></12a> <script></script> */
普通的标签可以直接通过绑定onclick的方式攻击,即便是img、video等资源加载标签,也可以通过onload、onerror等事件注入脚本,可见针对标签内属性的过滤也是不可或缺的。
然而本以为这样已足够,但是即使是只开放了class和style属性开放了,也是不安全的,关键在于style属性,如果任由用户自定义的话,可以通过style属性实现:点击劫持(将元素铺满整个界面)、加载外域图片、脚本注入甚至可以给文章设置一些花里胡哨的动画:
<div style="position:absolute;top:0;left:0;width:2000px;height:2000px;z-index:9999;"> 点击劫持的元素,阻止页面其他操作</div> <div style="background:url(javacript:alert('xss'))"> 借助style标签注入脚本,大部分xss过滤库会帮我们过滤这部分脚本</div> <div style="background:url(//xxx.com/H图.jpg)"> 偷偷加载其他网站的小H图,绕过过滤和审核</div>
可见style属性也不容忽视,因此我们需要在option参数中额外为style属性设置白名单,确保style属性安全可控:
// ... css: { whiteList: { color: true, 'background-color': true, }, }, // ...
其次为了确保不加载非本域名下的图片资源,我们也可以再这一层做一些针对img标签的过滤:
// ... stripIgnoreTag: true, onTagAttr: (tag:string, name:string, value:string, isWhiteAttr:boolean) => { // 判断img下的src属性 如果非本域名下 返回空 if (isWhiteAttr && tag === 'img' && name === 'src' && !checkLegal(value)) { return '#'; } }, // ...
3. cookie 设置HttpOnly,配合token或验证码防范
针对信息源的过滤,针对不可信数据源的过滤,已经能达到初步的效果,但这远远不够,毕竟没有绝对的安全。
由于大部分攻击者会想要获取到用户的cookie去做别的坏事,所以我们需要在http的响应头set-cookie时设置httpOnly,让浏览器知道不能通过document.cookie的方式获取到cookie内容。
app.get('/', (req, res) => { if(req.cookies.isVisit) { console.log(req.cookies) res.send('欢迎再次光临') } else { res.cookie('isVisit', 1, {maxAge: 3600 * 1000, httpOnly: true}) res.send('欢迎初次光临') } })
虽然避免了攻击者直接获取到cookie,但是攻击者仍然可以页面内发起别的请求,直接篡改用户的信息,因此需要我们配合token或者验证码的形式,防止攻击者直接通过脚本的方式篡改用户个人信息。而这个token类似于CSRF中我们所需要的token,不过如果攻击者仔细研究了代码,并且知道的token在页面中的来源,也是可以直接获取到token的,因此,相比之下验证码安全性更高。
4. 设置CSP安全策略
除了针对数据源的严格过滤以外,CSP安全策略的限制也是主要的XSS防范手段之一,通过在页面中设置允许加载的资源的来源,来严格限制页面可加载的脚本以及图片等资源,防止外部的脚本攻击后注入其他脚本以及内容。
CSP,内容安全策略,是一种基于内容的声明式网络应用程序机制,对缓解内容注入漏洞的危害非常有效。通过一系列指令告诉客户端(如浏览器)被保护资源(如页面)内只允许加载和执行指令集中限定的内容,类似白名单机制,不满足限定条件的资源和内容将被客户端阻断或不被执行。可以通过两种方式设置CSP,一种是meta标签,一种是HTTP响应头Content-Security-Policy:
指令及其说明:
- default-src 定义资源默认加载策略
- connect-src 定义Ajax、 WebSocket等加载策略
- font-src 定义Font 加载策略
- frame- src 定义Frame加载策略
- img-src 定义图片加载策略
- media- src 定义<audio>、 <video> 等引用资源加载策略
- object-src 定义 <applet>、 <embed>、 <object> 等引用资源加载策略
- script-src 定义JS加载策略
- style- src 定义CSS加载策略
接下来,以meta标签设置CSP为例,我们可以如下设置,以此来限制对白名单外资源加载:
<meta http-equiv="Content-Security-Policy" content= "script-src 'self' *.qq.com *.cdn-go.cn; img-src 'self' *.cdn-go.cn *.gtimg.cn data:; style-src 'unsafe-inline' *.cdn-go.cn; media-src 'none'; child-src *.qq.com">
参考:
https://github.com/leizongmin/js-xss/blob/master/README.zh.md
https://www.jianshu.com/p/4bad03d89c04