HttpOnly
浏览器禁止页面的JavaScript访问带有HttpOnly属性的Coockie。它解决的是XSS后的Cookie劫持攻击。
HttpOnly是在服务器返回的响应头Set-Cookie上标记的:
Set-Cookie: =[; =]
[; expires=][; domain=]
[; path=][; secure][; HttpOnly]
输入检查
XSS要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。
输入检查的逻辑,必须放在服务端代码中实现。如果只是在客户端使用JavaScript进行输入检查,很容易被攻击者绕过。目前的普遍做法是,同时在客户端和服务端实现相同的输入检查。客户端检查可以阻挡大部分误操作的用户,从而节省服务器资源。
XSS输入检查一般会检查用户输入的数据中是否包含一些特殊字符,如<、>、’、“等,将这些字符过滤或者编码。
比较智能的输入检查,可能会匹配XSS的特征。比如查找用户数据中是否包含了、javascript等敏感字符。
这种输入检查的方式,称为XSS Filter。
XSS Filter只获取了用户提交的数据进行检查,但是并没有结合渲染页面的HTML代码,因此对语境的理解并不完整。
例如:
用户只需要提交一个恶意脚本所在的URL地址,即可实施XSS攻击。而大多数情况下,URL是合法的用户数据。
XSS Filter还有一个问题,对<、>的处理可能会改变用户语义。
比如
1+1<3
如果XSS Filter不够智能,粗暴地过滤或者替换<,则会改变用户原来的意思。
输出检查
既然输入检查存在这么多问题,那输出检查又如何呢
一般来说,除了富文本输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
安全的编码函数
编码分为很多种,针对HTML代码的编码方式是HtmlEncode。
HtmlEncode并非专有名词,它只是一种函数实现。它的作用是将字符转换成HTMLEntities,对应的标准是ISO-8859-1
为了对抗XSS,在HTMLEncode中要求至少转换以下字符:
& -> &
< -> <
>
-> >
" -> "
' -> '
/ -> /
Javascript的编码方式可以使用JavaScriptEncode
它使用""对特殊字符进行转义。在对抗XSS时还要求输出的变量必须在引号内部,以避免造成安全问题。
比较两种写法
var x = escapeJavaScript($evil);
var y = '"'+escapeJavaScript($evil)+'"';
如果只是转义了几个危险字符,如’、”、<、>、、&、#等,那么可能会输出
var x = 1;alert(2);
var y = "1;alert(2);";
攻击者即使想要逃脱出引号的范围,也会遇到困难。
var y = "\";alert(1);\/\/";
但是开发者没有这个习惯怎么办?
那就只能使用一个更加严格的JavaScriptEncode函数来保证安全了——除了数字、字母外的所有字符,都使用十六进制“xHH”的方式进行编码
var x = 1;alert(2);
变成了
var x = 1\x3balert\x282\x29;
此外还有其他编码函数,如XMLEncode、JSONEncode等
只需要一种编码吗
XSS主要发生在MVC架构中的View层。大部分的XSS漏洞可以在模板系统中解决。
如Python的开发框架Django自带的模板系统"Django Templates",可以使用escape进行HtmlEncode,并且在Django1.0中得到了加强——默认所有的变量都会被escape。
但这样还是不能完全避免XSS问题,需要“在正确的地方使用正确的编码方式”
例如
test
如果用户输入
$var = htmlencode("');alert('2");
对于浏览去器来说,htmlparser会优先于JavaScript Parser执行
经过解析后,结果为
test
xss被注入。
xss发出的原因是,没有分清楚输出变量的语境,并非在模板引擎中使用了auto-escape就万事大吉了。
正确防御XSS
XSS的本质是一种“HTML注入”,想要根治XSS问题,可以列出所有XSS可能发生的场景,再一一解决。
在HTML标签中输出
$var
$var
防御的方法是对变量使用HtmlEncode
在HTML属性中输出
防御的方法是对变量也是使用HtmlEncode
在script标签中输出
首先应该确保输出的变量在引号中:
防御时使用JavaScriptEncode
在事件中输出
test
防御方式和在script标签中输出类似
在CSS中输出
比如@import、url等直接加载XSS、expression(alert('xss'))等
一方面需尽可能避免用户可控制的变量在css中输出,如果必须有这样的需求,那么使用encodeForCSS
在地址栏输出
一般来说使用URLEncode即可,但是如果整个URL被用户完全控制,Protocal和Host是不能使用URLEncode的,否则会改变URL的语义。[Protocal][Host][Path][Search][Hash]
例如https://www.evil.com/a/b/c/te...
对于如下的输出方式:
test
攻击者可能会构造伪协议实施攻击
test
此外,“vbscript”、“dataURI”等伪协议也可以导致脚本执行。
一般来说,对于用户控制整个URL,应该先检查是否以“http”开头,如果不是则自动添加,以保证不会出现伪协议类的XSS攻击,然后再对变量进行URLEncode。
处理富文本
网站允许用户提交一些自定义的HTML代码,称之为富文本。
在处理富文本时,还是要回到“输入检查”的思路上来。“输入检查”的主要问题是检查时还不知道变量的输出语境。但富文本数据,其语义是完整的HTML,在输出时也不会拼凑到某个标签的属性中。因此,可以特殊对待。
通过htmlparser可以解析出HTML代码的标签、标签属性和事件。
在过滤富文本时,事件应该被严格禁止、而一些危险的标签,比如iframe、script、base、form等也是应该严格禁止的。在标签选择上,应该使用白名单。比如,只允许a、img、div等比较安全的标签。“白名单”同样应该用于属性和事件的选择。
在富文本过滤中,处理CSS也是一件麻烦的事,因此应该禁止用户自定义CSS和style。如果不能禁止,那就需要一个CSS Parser对样式进行智能分析,检查其中是否包含危险代码。
防御DOM Based XSS
DOM Based XSS前文提到的几种防御方法都不太适用。
DOM Based XSS是从JavaScript中输出数据到HTML页面里,而前文都是针对从服务器应用直接输出到HTML页面的XSS漏洞。
服务器执行了javascriptEscape,输出的数据又重新渲染到了页面中,对变量进行了解码,仍然会产生XSS。
正确的防御方法是,在$var输出到script时,执行一次javaScriptEncode,其次在输出到HTML页面时,如果输出到事件或者脚本,则要再做一次javaScriptEncode;如果输出到HTML内容或者属性,则再做一次HtmlEncode。
触发DOM Based XSS的地方有很多,以下几个地方是JavaScript输出到HTML页面的必经之路。
document.write()
document.writeln()
xxx.innerHTML=
xxx.outerHTML=
innerHTML.replace
document.attachEvent()
window.attachEvent()
document.location.replace()
document.location.assign()
...
需要重点关注这几个地方的参数是否可以被用户控制
除了服务器端直接输出变量到JavaScript外,还有以下几个地方可能成为DOM Based XSS的输出点,也需要重点关注。
inputs框
window.location(href、hash等)
window.name
document.referrer
document.cookie
localstorage
XMLHttpRequest返回的数据
...
换个角度看XSS的风险
前面都是从漏洞的形成原理上看的,如果从业务风险角度看则有不同的观点
一般来说,存储型XSSS的风险高于反射型。因为它保存在服务器上,有可能会跨页面存在。它不会改变页面URL的原有结构,因此有时候还能逃过一些IDS的检测。
从攻击过程来说,反射型XSS,一般要求攻击者诱使用户点击一个包含XSS代码的URL链接;而存储型XSS,则只需要让用户查看一个正常的URL链接。
从风险角度看,用户之间有互动的页面,是可能发起XSS Worm攻击的地方。而根据不同页面的PageView高低,也可以分析出哪些页面受XSS攻击后的影响更大。
在修补漏洞时遇到的最大挑战之一是漏洞数量太多,开发者不太来得及立刻修复所有漏洞。从业务风险角度来重新定位每个XSS漏洞,就具有了重要意义。