前言 Web 安全的兴起
Web 攻击技术的发展也可以分为几个阶段。在
Web 1.0
时代,人们更多的是关注服务器端
动态脚本的安全问题,比如将一个可执行脚本(俗称
webshell
)上传到服务器上,从而获得权
限。后续有出现了
SQL
注入,
SQL
注入的出现是
Web
安全史上的一个里程碑,
SQL
注入漏洞至今仍然是
Web
安全领域中的一个重要组成部分。再后续另一个里程碑的安全问题问世–
XSS
(跨站脚本攻击)。伴随着
Web 2.0
的兴起,
XSS
、
CSRF
等攻击已经变得更为强大。
Web
攻击的思路也从服务器端转向了客户端,转向了浏览器和用户。
Web
技术发展到今天,构建出了丰富多彩的互联网。互联网业务的蓬勃发展,也催生出了
许多新兴的脚本语言,比如
Python
、
Ruby
、
NodeJS
等,敏捷开发成为互联网的主旋律。而手机技术、移动互联网的兴起,也给
HTML 5
带来了新的机遇和挑战。与此同时,
Web
安全技术,也将紧跟着互联网发展的脚步,不断地演化出新的变化。
跨站脚本攻击(
XSS
)是客户端脚本安全中的头号大敌。
OWASP TOP 10
威胁多次把
XSS
列在榜首,该篇文章将重点介绍
XSS
的攻防问题
初探
XSS
跨站脚本攻击,英文全称是
Cross Site Script
,本来缩写是
CSS
,但是为了和层叠样式表(
Cascading Style Sheet
,
CSS
)有所区别,所以在安全领域叫做“
XSS
”。
XSS
攻击,通常指黑客通过
HTML
注入 篡改网页,插入恶意脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击行为。在这种行为最初出现之时,所有的演示案例全是跨域行为,所以叫做 “跨站脚本” 。时至今日,随着
Web
端功能的复杂化,应用化,是否跨站已经不重要了,但
XSS
这个名字却一直保留下来。
随着
Web
发展迅速发展,
Web
开发已经被应用的非常广泛了,由之前的单一
PC
端扩展到现在的移动端(
APP
、
H5
),甚至包括桌面工具、设备大屏等等,所以在产生的应用场景越来越多,越来越复杂的情况下,同时大多数互联网(尤其是传统行业)的产品开发版本迭代上线时间非常短,一周一版本,两周一大版本的情况下,忽略了安全这一重要属性,一旦遭到攻击,后果将不堪设想。
XSS
攻击类型分类
XSS
攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于
DOM XSS
;
反射型
反射型
XSS
只是简单地把用户输入的数据“反射”给浏览器。也就是说,黑客往往需要诱使用户“点击”一个恶意链接,才能攻击成功。反射型
XSS
也叫做 **“非持久型
XSS
”(
Non-persistent XSS
)**。
通常反射型
XSS
的恶意代码存在
URL
里,通过
URL
传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的
URL
才能生效,攻击者往往会结合多种手段诱导用户点击。
一个最初级的反射型攻击是:我们对网页数据进行获取:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>XSS攻防演练</title></head><body><divid="t"></div><inputid="s"type="button"value="这是一个按钮"onclick="test()"></body><script>functiontest(){const arr =['自定义的数据1','自定义的数据2','自定义的数据3','<img src="11" οnerrοr="console.log(window.localStorage)" />']const t = document.querySelector('#t')
arr.forEach(item=>{const p = document.createElement('p')
p.innerHTML = item
t.append(p)})}</script></html>
当黑客点击
这是一个按钮
时,即可轻松获取本地
localStorage
数据,从而获取关键信息。
存储型
存储型
XSS
会把用户输入的数据“存储”在服务器端。这种
XSS
具有很强的稳定性。
比较常见的一个场景就是,黑客写下一篇包含有恶意
JavaScript
代码的博客文章,文章发表后,所有访问该博客文章的用户,都会在他们的浏览器中执行这段恶意的
JavaScript
代码。黑客把恶意的脚本保存到服务器端,所以这种
XSS
攻击就叫做 **“存储型
XSS
”**。
<!-- 例如我们分别在网站中的输入框中输入以下信息,并保存到远程数据库 --><imgsrc="11"onerror="console.log(window.localStorage)"/><imgsrc="11"onerror="alert(111)"/>
页面输入
使用者浏览页面,分别先后触发了
alert
弹框和
localStorage
获取本地数据:
以上就是一个典型的存储型攻击。
基于
DOM XSS
实际上,这种类型的
XSS
并非按照“数据是否保存在服务器端”来划分,
DOM Based XSS
从效果上来说也是反射型
XSS
。单独划分出来,是因为
DOM Based XSS
的形成原因比较特别,发现它的安全专家专门提出了这种类型的
XSS
。
DOM 型 XSS
跟前两种
XSS
的区别:
DOM 型 XSS
攻击中,取出和执行恶意代码由浏览器端完成,属于前端
JavaScript
自身的安全漏洞,而其他两种
XSS
都属于服务端的安全漏洞。
接下来我们来看一个简单的示例:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>XSS攻防演练</title></head><body><h3>基于DOM的XSS</h3><inputtype="text"id="input"><buttonid="btn">提交内容</button><divid="div"></div></body><script>const input = document.getElementById('input');const btn = document.getElementById('btn');const div = document.getElementById('div');let inputValue;
input.addEventListener('change',(e)=>{
inputValue = e.target.value;},false);
btn.addEventListener('click',()=>{
div.innerHTML =`<a href=${inputValue}>链接地址</a>`},false);</script></html>
我们再页面输入框中输入以下文本
'' onclick=alert(/xss/)
,这里的
''
引号是为了关闭掉
href
属性,给它赋予了一个空值。然后点击
提交内容
按钮,则页面中的
<div id="div"></div>
标签包含了一下
html
内容
<ahrefonlick="alert(/xss/)">链接地址</a>
XSS
攻击防御
关于
XSS
的防御是非常复杂的,值得幸运的是现代浏览器、前端框架/库已经帮我们做了相当大的一部分工作。
HttpOnly
HttpOnly
最早是由微软提出,并在
IE 6
中最先实现的,至今已经逐渐成为一个标准。浏览器将禁止页面的
JavaScript
访问带有
HttpOnly
属性的
Cookie
。所以我们需要在
http
的响应头
set-cookie
时设置
httpOnly
,让浏览器知道不能通过
document.cookie
的方式获取到
cookie
内容。
严格地说,
HttpOnly
并非为了对抗
XSS——HttpOnly
解决的是
XSS
后的
Cookie
劫持攻击。所以说使用
HttpOnly
有助于缓解
XSS
攻击,但仍然需要其他能够解决
XSS
漏洞的方案;
输入检查
对于用户的输入内容我们需要持怀疑态度。在对输入不做任何过滤检查的情况下用户可能输入任何字符串。比如我们期望输入的内容是:
hello word
, 也许我们收到的内容是
onclick=alert(/xss/)
。
在
XSS
的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如
<、>、’、”
等。如果发现存在特殊字符,则将这些字符过滤或者编码。这种输入检查的方式,可以称为
“XSS Filter”
。互联网上有很多开源的
“XSS Filter”
的实现。比如一个简单的
htmlencode
转义:
consthtmlEncode=function(handleString){return handleString
.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(//g," ").replace(/\'/g,"'").replace(/\"/g,""");}
但是输入检查也有弊端,比如
- 攻击者绕过前端页面直接使用接口就可以提交恶意代码到远程库中。
- 输入数据,还可能会被展示在多个地方,每个地方的语境可能各不相同,如果使用单一的替换操作,则可能会出现问题。输入检查也需要有针对性,如果我们想表达的意思是一个数小于另一个数(
3 < 4
),前端转义后的字符就变成3 < 4
,当这个值被存到远端时后,再通过AJAX
获取使用就会造成不必要的麻烦,比如我就进行数值计算等等。
输出检查
一般来说,除富文本的输出外,在变量输出到
HTML
页面时,可以使用编码或转义的方式来防御
XSS
攻击。
XSS的本质还是一种“HTML 注入”,用户的数据被当成了HTML代码一部分来执行,从而混淆了原本的语义,产生了新的语义。
如同输入检查一样,我们可以对输出进行编码转义。
1.在
HTML
中输出
比如我们的html代码中有这样一段代码:
<div>$htmlVar</div><ahref="">$htmlVar</a>
如果输出的变量没有进行安全处理,直接使用并渲染在页面中,都能导致直接产生
XSS
。最终的结果可能生成一下代码:
<div><script>alert('我是一个XSS攻击者')</script></div><ahref="#"><imghref=""onclick="alert('我是另外一个XSS攻击者')"></a>
这个预防的方法就是对html进行转义检查
2. 在
HTML
属性中输出
如果我们的html属性时动态值,那么利用属性也可以被攻击;
<divid="testXSS"data-name=""></div>
现在往
data-name
属性中插入一段未转义的代码
"><script>alert('我是一个XSS攻击者')</script><"
,结果如下:
<divid="testXSS"data-name=""><script>alert('我是一个XSS攻击者')</script><""></div>
3. 在
<script>
标签中输出
在
<script>
标签中输出时,首先应该确保输出的变量在引号中。
<script>// 假设userData是攻击者注入的数据let xssVar = userData;</script>
攻击者需要先闭合引号才能实施XSS攻击:
<script>// 假设userData是攻击者注入的数据let xssVar ="";alert('我是一个script XSS攻击者');</script>
4. 在
CSS
中输出
在
CSS
和
style
、
style attribute
中形成
XSS
的方式非常多样化,所以,一般来说,尽可能禁止用户可控制的变量在“
<style>
标签”、“
HTML
标签的
style
属性”以及“
CSS
文件”中输出。如果一定有这样的需求,则推荐使用一个关于CSS转义库。
防御
DOM Based XSS
DOM Based XSS
是一种比较特别的
XSS
漏洞,前文提到的几种防御方法都不太适用,需要特别对待。这个本质上,实际上就是网站前端JavaScript代码本身不够严谨,把不可信的数据当作代码执行了。
如果用
Vue/React
技术栈,并且不使用
v-html
/
dangerouslySetInnerHTML
功能,就在前端
render
阶段避免
innerHTML
、
outerHTML
的
XSS
隐患。稍后会有专门的
Vue
关于
XSS
的防御段落。
会触发
DOM Based XSS
的地方有很多,以下几个地方是
JavaScript
输出到
HTML
页面的必经之路。
document.write()
;document.writeln()
;xxx.innerHTML()
;xxx.outerHTML()
;xxx.innerHTML.replace()
;document.attachEvent()
;window.attachEvent()
;window.location()
;window.name
;
所以开发者需要重点关注这几个地方的参数是否可以被用户控制。如果项目中有用到这些的话,一定要避免在字符串中拼接不可信数据。
Vue
中的
XSS
防御
如果你在项目中使用了
Vue
作为前端开发框架,恭喜你,
Vue
将为你解决绝大多数的
XSS
攻击问题,但是
Vue
不是一个预防
XSS
攻击的框架,在开发使用的时候还是有被攻击的漏洞存在;
Vue
中的防御措施
不论使用模板还是渲染函数,
Vue
都会将插值的内容都会自动转义。也就是说对于这份模板:
<template><p>{{userData}}</p></template><script>// 从远程获取的数据
userData = "<script>alert('xss')</script>"
</script>
最终编译后页面显示的
html
源码内容如下:
<p><script>alert('xss')</script></p>
原因是
Vue
帮我们对数据进行了转义,因此避免了脚本注入。该转义通过诸如 textContent 的浏览器原生的 API 完成,所以除非浏览器本身存在安全漏洞,否则不会存在安全漏洞。转义后的内容如下:
<script>alert("xss")</script>
注入
HTML
如果你要动态注入远程的
HTML
内容,首先你应该确保这些内容是安全有效的,否则你应该采取一些防御措施,去过滤或转义掉一些危险的标签符号;例如你可以这样显示的渲染
HTML
:
<!-- 当使用模版时 --><divv-html="userProvidedHtml"></div><!-- 当使用渲染函数时 --><script>h('div',{domProps:{innerHTML:this.userProvidedHtml
}})</script><!-- 当使用JSX 的渲染函数时 --><divdomPropsInnerHTML={this.userProvidedHtml}></div>
例如我们可以使用一个简单的方法(或者引用一个更加健壮的库/插件XSS来过滤一遍这个远程的
userProvidedHtml
数据内容,以确保安全;
// 一个简单的函数,通过转义<为<以及>为>来实现防御HTML节点内容constescape=function(str){return str.replace(/</g,'<').replace(/>/g,'>')}
样式注入
在使用
Vue
要在模板内避免渲染
style
标签:
<style>{{ userProvidedStyles }}</style>
这是因为,一但通过
userProvidedStyles
,恶意用户仍可以提供
CSS
来进行“点击诈骗”,例如将链接的样式设置为一个透明的方框覆盖在“登录”按钮之上。然后再把
https://user-XSS-website.com/
做成你的应用的登录页的样子,它们就可能获取一个用户真实的登录信息,所以Vue推荐使用
对象语法
且只允许用户提供特定的可以安全控制的
property
的值:
<!-- sanitizedUrl应为受控的地址 --><av-bind:href="sanitizedUrl"v-bind:style="{
color: userProvidedColor,
background: userProvidedBackground
}">
click me
</a>
安全问题“没有银弹”
在解决安全问题的过程中,不可能一劳永逸,也就是说“没有银弹”。
一般来说,人们都会讨厌麻烦的事情,在潜意识里希望能够让麻烦越远越好。而安全,正是一件麻烦的事情,而且是无法逃避的麻烦。任何人想要一劳永逸地解决安全问题,都属于一相情愿,是“自己骗自己”,是不现实的。
最佳实践
通用的规则是只要允许执行未过滤的用户提供的内容 (不论作为
HTML
、
JavaScript
甚至
CSS
),你就可能令自己处于被攻击的境地。这些建议实际上不论使用
Vue
、
React
还是别的框架甚至不使用框架,都是成立的。
参考文献
- 白帽子讲Web安全
- Vue官方文档
版权归原作者 绝对零度HCL 所有, 如有侵权,请联系我们删除。