我们对XSS的防御办法一般会采取针对关键敏感字符进行编码的方式,被称为XSS终结者的CSP先暂且不提,大部分人都会采用htmlencode的方式来限制危险字符输出,但这真的足够了吗?
在web2py(python的Web框架)的安全文档中说将所有变量在视图层进行eacape,可以防御XSS,看似好像并没有什么不对,但如果出现以下情况呢?
<a href=# onclick="alert('$var');">XSS Test</a>
$var是获取用户的输入,在进行渲染到浏览器之前,web2py框架会对它进行htmlencode编码,假设用户输入为:
$var = ');alert(666
编码之后,结果为:
');alert('666
此时,DOM的渲染结果是这样的:
<a href=# onclick="alert('');alert('666');">XSS Test</a>
这样转码照常理来说应该肯定不会有问题了吧?人家都输出实体编号了,怎么XSS。但是,你点一下试试,看会不会弹两个窗。
结果弹了!而且HTML编码分三种,实体编号十进制表示,实体编号十六进制表示,实体名称表示,上面的就是十六进制表示方式。其中任何一种编码都可以弹窗,为什么?
因为浏览器在JavaScript执行之前对它进行自解码了,并且,在这里解码的方式还有很大区别。
这里的关键就是上下文语境,什么意思?就是变量的位置附近对应的是HTML标签还是JavaScript代码,如果是像上文一样的HTML标签(a标签),那么浏览器在执行JavaScript语句之前会优先执行HTML解码操作,所以上面web2py框架的所谓escape防御根本没用。
那什么样的语境是优先进行JavaScript解码呢?类似下面这样:
function $(name){
return document.getElementByTagName(name);
}
$('a').onclick = function(){
alert('');alert('666');
}
这种情况上下文语境就会优先进行JS解码,所以上面点击以后不会弹两次窗。
如果你把上面的十六进制HTML编码换成JS的编码,会弹两次窗吗?不会。因为虽然自解码,但是被当成了字符串,同时这也是防御用户输入变量出现在js环境中被XSS的办法,进行JS编码。
那怎么才能弹呢?像下面这样,JS的自解码会成为帮凶:
document.write('\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x23\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29\x3e');
以上部分被解码后是:
<img src=# onerror=alert('xss')>
之后被渲染到页面,完成弹窗。
JS有哪些编码呢?
- 四位Unicode 形式:\u ,不足补零
- 十六进制:\xH
- 八进制:\
- 纯转义:\’、\”、\<、>等
所以,单纯的htmlencode并不能彻底防御XSS,要根据实际的语境来决定编码方式。
与之类似的,在CSS中也有这种自解码次序引发的问题,下面两个CSS语句在IE中效果是相同的:
color: expression(alert(1))
color: expression\028 alert \028 1 \029 \029
参考:
道哥 《白帽子讲Web安全》
余弦 《Web前端黑客技术揭秘》