详谈XSS防御方法

 
1、HttpOnly
严格的说,httponly并非为了对抗XSS,它解决的是XSS后的Cookie劫持攻击。Cookie设置了httponly之后,JavaScript读不到该cookie的值。
一个cookie的使用过程如下:
step1:浏览器向服务器发起请求,这时候没有cookie
step2:服务器返回时发送set-cookie头,向客户端浏览器写入cookie
step3:在该cookie到期前,浏览器访问该域下的所有页面,都将发送该cookie
HttpOnly是在set-cookie时标记的:
Set-Cookie: <name>=<value>[; <Max-Age>=<age>]
[; expires=<date>][; domain=<domain_name>]
[; path=<some_path>][; secure][; HttpOnly]
HttpOnly可以有选择性地加在任何一个cookie值上:在某些时候,应用可能需要JavaScript访问某几项cookie,这种cookie可以不设置HttpOnly标记,而仅把HttpOnly标记给用于认证的关键cookie
举个例子:
 
<?php
header("Set-Cookie: cookie1=test1;");
header("Set-Cookie: cookie2=test2;httponly",false);
?>
 
<script>
alert(document.cookie);
</script>

 

 
在这段代码中,cookie1没有HttpOnly,cookie2被标记为HttpOnly,两个cookie均被写入浏览
但是只有cookie1被JavaScript读取到:
这就是HttpOnly的作用。
 
 
2、输入检查
 
输入检查的逻辑,必须放在服务器端代码中实现。如果只是在客户端使用JavaScript进行输入检查,是很容易被攻击者绕过的。目前Web开发的普遍做法,是同时在客户端JavaScript中和服务器端代码中实现相同的输入检查。客户端JavaScript的输入检查,可以阻挡大部分误操作的用户,从而节约服务器资源。
 
3、输出检查
 
一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
针对HTML代码的编码方式是HtmlEncode
HtmlEncode并非专有名词,它只是一种函数实现。它的作用是将字符转化成HTMLEntities。
为了对抗XSS,在HtmlEncode中要求至少转换一下字符:
&    -->   &amp;
<    -->   &lt;
>    -->   &gt;
"     -->   &quot;
'      -->   &#x27;           $apos;不推荐
/     -->   &#x2F;            包含斜杠是因为它可能会闭合一些HTML entity
在PHP中,有htmlentities()和htmlspecialchars()两个函数可以满足安全要求。
htmlspecialchars — 将特殊字符转换为 HTML 实体
htmlentities — 将字符转换为 HTML 转义字符,本函数各方面都和 htmlspecialchars() 一样, 除了 htmlentities() 会转换所有具有 HTML 实体的字符。
 
相应地,JavaScript的编码方式可以使用JavaScriptEncode
JavaScriptEncode与HtmlEncode的编码方式不同,它需要使用反斜杠"\"对特殊字符进行转义。
在对抗XSS时,还要求输出的变量必须在引号内部,以避免造成安全问题。比较下面两种写法:
var x = escapeJavascript($evil);
var y = '"'+escapeJavascript($evil)+'"';
如果escapeJavascript()函数只转义了几个危险字符,比如 ' "  <  >  \   &  #  等,那么上面的两行代码输出后可能会变成:
var x = 1;alert(2);
var y = "1;alert(2)";
第一行执行了额外的代码了;第二行则是安全的。对于后者,攻击者即使想要逃逸出引号的范围,也会遇到困难:
var y = "\";alert(1);\/\/";
所以要求JavaScript的变量输出一定要在引号内
可是很多开发者没有这个习惯怎么办?这就只能使用一个更加严格的JavaScriptEncode函数来保证安全——除了数字、字母外的所有字符,都使用十六进制“\xHH”的方式进行编码
在本例中:
var x = 1;alert(2);
变成了:
var x = 1\x3balert\x282\x29;
如此代码可以保证是安全的。
 
此外,XSS是很复杂的问题,需要"在正确的地方使用正确的编码方式"。 举个例子:
<body>
<a href=# onclick="alert('$var');" >test</a>
</body>
开发者希望看到的结果是,用户点击链接后,弹出变量"$var"的内容。可是如果用户输入:
$var = htmlencode("');alert('2");
对变量"$var"进行htmlencode后,渲染的结果是:
<body>
<a href=# onclick="alert('&#x27;&#x29;&#x3b;alert&#x28;&#x27;2');">test</a>
</body>
对于浏览器来说,htmlparser会优先于JavaScript Parser执行,所以解析过程是,被HtmlEncode的字符先被解码,然后执行JavaScript事件。
因此,经过htmlparser解析后相当于:
<body>
<a href=# onclick="alert('');alert('2');">test</a>
</body>
成功在onclick事件中注入了XSS代码。
第一次弹框
第二次弹框
导致XSS攻击发送的原因,是由于没有分清楚输出变量的语境。
 
4、正确地防御XSS
 
XSS的本质还是一种"HTML注入",用户的数据被当成了HTML代码一部分来执行,从而混淆了原本的语义,产生了新的语义。
下面将用变量"$var"表示用户数据,它将被填充入HTML代码中。可能存在以下场景。
 
(1)、在HTML标签中输出
<div>$var</div>
<a href=# >$var</a>
所有在标签中输出的变量,如果未做任何处理,都能导致直接产生XSS。
在这种场景下,XSS的利用方式一般是构造一个<script>标签,或者是任何能够产生脚本执行的方式。比如:
<div><script>alert(/xss/)</script></div>
或者
<a href=# ><img src=# onerror=alert(1) /></a>
防御方法是对变量使用HtmlEncode
 
(2)、在HTML属性中输出
<div id="abc" name="$var" ></div>
与在HTML标签中输出类似,可能的攻击方式是:
<div id="abc" name=""><script>alert(/xss/)</script><""></div>
防御方法也是采用HtmlEncode
在OWASP ESAPI中推荐了一种更严格的HtmlEncode——除了字母、数字外,其他所有的特殊字符都被编码成HTMLEntities
String safe = ESAPI.encoder().encodeForHTMLAttribute(request.getParameter("input"));
这种严格的编码方式,可以保证不会出现任何安全问题。
 
(3)、在<script>标签中输出
在<script>标签中输出时,首先应该确保输出的变量在引号中:
<script>
var x = "$var";
</script>
攻击者需要先闭合引号才能实施XSS攻击:
<script>
var x= "";alert(/xss/);//";
</script>
防御时使用JavascriptEncode
 
(4)、在事件中输出
在事件中输出和在<script>标签中输出类似:
<a href=# onclick="funcA('$var')" >test</a>
可能的攻击方法:
<a href=# onclick="funcA('');alert(/xss/);//')" >test</a>
在防御时需要使用JavascriptEncode
 
(5)、在CSS中输出
 
在CSS和style、style attribute中形成XSS的方式非常多元化,参考下面几个XSS的例子:
<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>
<STYLE>BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}</STYLE>
<XSS STYLE="behavior: url(xss.htc);">
<STYLE>li {list-style-image: url("javascript:alert('xss')");}>/STYLE><UL><LI>XSS
<DIV STYLE="background-image: url(javascript:alert('XSS'))">
<DIV STYLE="width: expression(alert('XSS'));">
所以,一般来说,尽可能禁止用户可控制的变量在"<style>标签"、“HTML标签的style属性”以及“CSS文件”中输出。如果一定有这样的需要,则推荐使用OWASP ESAPI中的encodeForCSS()函数
String safe = ESAPI.encoder().encodeForCSS(request.getParameter("input"));
其实现原理类似于ESAPI.encoder().encoderForJavaScript()函数,除了字母、数字外的所有字符都被编码成十六进制形式"\uHH"
 
(6)、在地址中输出
 
一般来说,在URL的path(路径)或者search(参数)中输出,使用URLEncode即可。
但是还有一种情况,就是整个URL能够被用户完全控制。这时URL的protocal和Host部分是不能够使用URLEncode的,否则会改变URL的语义。
一个URL的组成如下:
[protocal][host][path][search][hash]
例如:
https://www.evil.com/a/b/c/test?abc=123#ssss
[protocal] = "https://"
[host] = "www.evil.com"
[path] = "/a/b/c/test"
[search] = "?abc=123"
[hash] = "#ssss"
在protocal与host中,如果使用严格的URLEncode函数,则会把"://"、"."等都编码掉。
 
对于如下的输出方式:
<a href="$var" >test</a>
攻击者可能会构造伪协议实施攻击:
<a href="javascript:alert(1);" >test</a>
除了"javascript"作为伪协议可以执行代码外,还有"vbscript"、“dataURI”等伪协议可能导致脚本执行。
“dataURI”这个伪协议是Mozilla所支持的,能够将一段代码写在RUL里。如下例:
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTs8L3NjcmlwdD4=">test</a>
这段代码的意思是,以test/html的格式加载编码为base64的数据,加载完成后实际上的:
<script>alert(1)</script>
点击<a>标签的链接,将导致执行脚本。
 
由此可见,如果用户能够完全控制URL,则可以执行脚本的方式有很多。如果解决这种情况呢?
一般来说,如果变量是整个URL,则应该先检查变量是否以"http"开头(如果不是则自动添加),以保证不会出现伪协议类的XSS攻击。
<a href="$var" >test</a>
在此之后,再对变量进行URLEncode,即可保证不会再有此类的XSS发生了。
 
5、处理富文本
 
有些时候,网站需要允许用户提交一些自定义的HTML代码,称之为“富文本”。
过滤富文本时,“事件”应该被严格禁止,因为“富文本”的展示要求里不应该包含“事件”这种动态效果。而一些危险的标签,比如<iframe>、<script>、<base>、<form>等,也是应该严格禁止的。
在标签的选择上,应该使用白名单,避免使用黑名单。
 
6、防御DOM Based XSS
 
DOM Based XSS是从JavaScript中输出数据到HTML页面里。而前文提到的方法都是针对“从服务器应用直接输出到HTML页面”的XSS漏洞,因此并不适用于DOM Based XSS。
例如:
<script>
var x="$var";
document.write("<a href='"+x+"'>test</a>");
</script>
变量"$var"输出在<script>标签里,可是最后又被输出到HTML页面中。
假设为了保护"$var"直接在<script>标签中产生XSS,服务器端对其进行了javascriptEscape。可是,$var在document.write时,仍然能够产生XSS,如下所示:
<script>
var x="\x20\x27onclick\x3dalert\x281\x29\x3b\x2f\x2f\x27";
document.write("<a href='"+x+"'>test</a>");
</script>
页面渲染效果之后的实际结果如下:
XSS攻击成功:
其原因在于,第一次执行javascriptEscape之后,只保护了
var x="$var";
但是当document.write输出数据到HTML页面时,浏览器重新渲染了页面。在<script>标签执行时,已经对变量x进行了解密,其后document.write再运行时,其参数就变成了:
<a href='_' onclick="alert(1);//'' ">test</a>
xss因此而产生。
 
那是不是因为对"$var"用错了编码函数呢?如果改成HtmlEncode会怎么样?继续看下面的这个例子:
<script>
var x="1&#x22;&#x29;&#x3b;alert&#x28;2&#x29;&#x3b;&#x2f;&#x2f;&#x22;";
document.write("<a href=# onclick='alert(\""+x+"\")'>test</a>");
</script>
服务器把变量HtmlEncode后再输出到<script>中,然后变量x作为onclick事件的一个函数参数被document.write到了HTML页面里
onclick事件执行了两次"alert",第二次是被XSS注入的。
 
那么正确的防御方法是什么呢?
首先,在"$var"输出到<script>时,应该执行一次javascriptEncode;其次,在document.write输出到HTML页面时,要分具体情况看待:如果输出到事件或者脚本中,则要再做一次javascriptEncode;如果是输出到HTML内容或属性,则要做一次HtmlEncode
也就是说,从JavaScript输出到HTML页面,也相当于一次XSS输出的过程,需要分语境使用不同的编码函数。
 
会触发DOM Based XSS的地方很多,以下几个地方是JavaScript输出到HTML页面的必经之路:
  • document.write()
  • document.writeIn()
  • 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.referer
  • document.cookie
  • localstorage
  • XMLHttpRequest返回的数据
posted @ 2020-04-07 20:42  yokan  阅读(2457)  评论(0编辑  收藏  举报