浏览器工作原理与XSS-HTML编码

 简介

编码问题一直是个痛点,尤其是当我们对XSS攻击原理不是很熟悉的话,防护起来很容易造成遗漏。要想很好的防护住XSS攻击,需要对浏览器解析HTML、JS、CSS的原理弄清楚,了解浏览器的工作原理,才能做好防护工作。小编也是搜集网上资料进行学习,并整理分享下该篇文档。


(一)浏览器的结构 

浏览器的主要组件,包含用户界面、浏览器引擎、呈现引擎、网络、用户界面后端、JavaScript解释器、数据存储。这里我们主要需要了解呈现引擎,其主要负责显示请求内容,如果请求的内容是HTML,它就负责解析HTML和CSS内容,并将解析后的内容显示在屏幕上。

值得注意的是,Chrome浏览器的每个标签页都分别对应一个呈现引擎实例,每个标签页都是独立的进程。

呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。

呈现引擎解析HTML文档,将标记转换成内容树上的DOM节点,将CSS元素样式转换成另外的树结构:呈现树。构建完成后,进入布局阶段,每个节点都会分配一个应该出现在屏幕上的坐标,由用户界面后端层将每个节点绘制出来。

解析的过程其实就是编译原理那一套东西,由解析器和词法分析器将文档内容构造成一个有效的解析树,最后由翻译器,将解析树翻译成浏览器可执行的机器指令,最后我们看到的就是一个可视化的web页面。

HTML解析器的任务是将HTML标记解析成解析树,常规解析器是不适用与HTML的,因为我们知道HTML具有很强的容错性,并不是与上下文无关的语法,我们会通过document.write添加额外的标记。HTML的定义采用了DTD格式,包括允许使用的元素及其属性和层次结构。HTML的解析是浏览器通过标记化、树结构的形式完成解析构建的。

CSS是上下文无关的语法,可以使用常规的解析器进行解析。  关于具体如何解析,便不在这里阐述,大家只需要有个大致的了解即可。

处理脚本和样式表的顺序

HTMl==》CSS==》JavaScript 针对这个顺序我是报迟疑态度的,因为上述我们也提到过,在遇到<script>脚本时会立即解析并执行脚本,文档解析将停止,直到脚本执行完毕。如果脚本是外部的,解析过程会停止,直到从网络同步抓取资源完成后再继续。在HTML5中增加了一个选项,可将脚本标记为异步,以便由其它线程解析和执行。

 (二)浏览器解码过程 

HTMl==》CSS==》JavaScript ,针对这个解析顺序,我们这里可以先不讨论,我们的目的是关心如何正确的编码,那么我只需要关注,对于常用的HTML页面,浏览器是如何解码的即可。

1、HTML实体

<p>${content}</p>

如上述代码所示,在P标签中存在一个输出变量${content},浏览器解析的过程,首先是HTML解析,解析到P标签时,解析Content的内容,然后将其在页面显示出来。

<p><script>alert("实体XSS");</script></p>

如果我们把Content的内容换成上面内容,即script脚本,那么浏览器解析的时候,当解析到P标签时,发现里面的内容存在script标签,便会把其当做JavaScript脚本进行解析,从而达到XSS攻击的目的。

 所以针对此类HTML实体间的输出,我们希望输出的是HTML文本内容,而不是HTML标签、JS代码等,所以我们在输出时,需要对Content进行HTML编码,可使用OWASP ESAPI的ESAPI.encoder().encodeForHTML()。 HTML编码一般将如下几个字符进行编码替换:

1. & —> &amp;

2. < —> &lt;

3. > —> &gt;

4. " —> &quot;

5. ' —> &#x27;

6. / —> &#x2F;

在编码的字符中,其中&、<、>、"、' 五个字符是XML中定义的实体,所以我们需要对其进行编码,因为HTML也算作XML的一种,/ 字符作为HTML标签的结束协助符,避免破坏标签。

HTML编码的作用就是将原本能被HTML解析成标签的东西,转换成字符串文本,以文本的形式展现

2、HTML通用属性

<input name="${firstname}" ></input>

name是input的属性,所以HTML解析时,会对name属性的内容进行HTML解码,假如此时${firstname}的值为如下内容时,HTML属性被截断插入了onclick事件。

<input name=" " onclick="alert('属性XSS')" " "></input>

所以针对这种常规的HTML属性,都需要对其进行HTML属性编码,其实和HTML实体编码类似,只不过编码字符范围可能多些,除了字母数字外,所有ASCII码小于256的字符均进行&#XXX;编码,具体的参见ESAPI.encoder().encodeForHTMLAttribute的实现。其实属性XSS中最主要的是拆分属性标签,注入可执行JS的属性,并对其添加而已代码。

对于通用属性的编码方式不适用于href、src、style、事件处理函数(onclick、onmouseover等),因为本身这些属性是支持伪协议的,该编码无效。

3、支持协议解析的HTML属性

在上述的2中场景中我们都可以使用HTML编码,基本都能够很好的防御住XSS攻击,在第二点中我们强调的是HTML通用属性,而并非全部属性,因为在HTML中H还存在许多支持协议解析的HTML属性,如onclick,onerror,href,src等,类似这种属性是无法通过HTML编码防范XSS攻击,因为浏览器会先解析HTML编码的字符,将其转换为该属性的值,但是该属性本身支持JS代码执行,所以浏览器在HTML解码后,对该属性的值进行JS解析,故会执行相应的代码。

3.1 href属性引入的XSS

<a href="javascript:alert('href xss')" target="_blank">href xss</a>

<a href="javascript&#x3a;alert&#x28;&#x27;href&#x20;xss&#x20;HTML编码无效&#x27;&#x29;" target="_blank">href xss HTML属性编码无效</a>

href属性的值应该是一个有效的URL链接,如果我们对整个href属性值进行URL编码,则会导致URL无法跳转,应为浏览器在解析href时,会通过分号、/字符解析出协议或目录,当对全部的值进行URL编码后,则无法进行正常解析;如果对协议后的内容进行URL编码则也无法防御XSS攻击,如下例所示:

<a href="javascript:%61%6c%65%72%74%28%27%68%72%65%66%20%78%73%73%20URL部分编码无效%27%29" target="_blank">Href xss 部分URL编码无效</a>

此时假如我们对alert('href xss')进行JavaScript编码,结果又会如何?(JavaScript编码将字符编码成\x+16进制的形式,对款字节编码成Unicode)

<a href="javascript:alert\x28\x27href xss\x27\x29" target="_blank" >Href XSS JavaScript编码</a>

测试点击没有任何反应,XSS执行失败;

问什么会存在上述的结果?我们看一下href属性的解码解析过程,页面渲染,首先进行HTML解码解析,解析出是href属性后(点击),会对href的值进行URL解码解析,获取到URL的实际值,当发现不是HTTP/HTTPS,而是JavaScript协议后,就会执行JavaScript解码解析,从而执行了alert()函数。

3.2 onclick属性XSS

现在我们来看一下on事件属性:<p id="addlinecontent" onclick="addlinecontent($value)">点击增加一行显示</p> (此处的$value往往一般都是后台模板替换的变量)

<p id="addlinecontent" onclick="addlinecontent('$value')">点击增加一行显示</p>

<script>

function addlinecontent(value){

    document.getElementById("addlinecontent").innerText=value;

}

</script>

当$value的值 hello world'),alert('onclick xss 时,出发XSS攻击:

<p id="addlinecontent" onclick="addlinecontent('hell0 world'),alert('onclick xss')" >

对$value进行HTML编码:hello world&#x27;&#x29;&#x2c;alert&#x28;&#x27;onclick xss htmlencode ,结果显示如下,存在XSS

此时如果将$value进行JavaScript编码:显示正常,不存在XSS

onclick属性解析的过程:先进行HTML解析,再进行JavaScript解析,所以仅仅进行HTML编码是无法防御XSS的,需要对值进行JavaScript编码。

如果当存在 <input name="username" onclick="$event" />这种情况时,是很难进行防护的,因为无法直接对$event进行JavaScript编码,如果编码了,则无法解析出需要执行的函数代码。如果整个内容是来自客户端输入的,那么需要对event需要进行黑白名单的校验,防止出现危险字符,防止调用危险的JS函数。通常不建议出现这种写法,建议对event进行拆分,通过控制参数达到相应的目的:

<input name="" onclick="event(encodeForJS(${eventType}))" />

关于如何编码,需要抓住一个重点,就是传进的值最终是谁消费(期望执行者),谁消费谁负责;对于支持伪协议的属性,仅仅通过编码是不行的,需要匹配过滤协议头,最好后台拆分拼接返回期望的值。(不能对协议类型进行任何的编码操作,否则URL解析器会认为它无类型)

编码的顺序和解码解析的顺序正好相反:

<p onclick="alert('\x26\x23\x78\x32\x32\x3b')">jsencode(htmlencode("))</p>

<p onclick="alert('&#x5c;&#x78;&#x32;&#x32;')">htmlencode(jsencode("))</p>

4、DOM XSS类型

DOM XSS是基于文档对象模型的XSS,属于反射型XSS。

1. 使用document.write直接输出

2. 使用innerHTML直接输出

3. 使用location、location.href、location.replace、iframe.src、document.referer、window.name等

.......

造成DOM XSS的原因主要是在重新修改页面时,没有考虑到对变量的编码和校验过滤;

<script>

    document.body.innerHTML="url:<a href=' "+url+" '>" +url+"</a> ";

</script>

其中对于变量url则是注入点:javascript:alert('dom xss');  

对于DOM XSS主要是由于本地客户端获取DOM数据在本地执行导致的,所以在编码中,要避免客户端文档重写,重定向或其它修改DOM元素操纵,对于输出至HTML中的值进行HTML编码,输出至JS中的值进行JS编码。

5、setTimeout/setInterval/eval等XSS

针对这类的XSS,我们单独拿出来说,因为这类XSS隐藏的相对比较深,尤其经过JS层层封装的函数,我们不知道底层是否使用了这些函数,是否存在变量传入?

这类函数有一个共性,函数本身就是JS函数,但是能够将传入的参数当做JS代码执行。

<textarea id="settimeoutxss" onclick="updatecontent('$value')" rows="8" cols="40"></textarea>

<script>

function updatecontent(url){

    setTimeout("showURL('"+url+"')");

}

function showURL(url){

    document.getElementById("settimeoutxss").value=url;

}

</script>

当$value的值为:hello world\'\);alert\(0\);eval(\' ,XSS攻击成功

当你在onclick="updatecontent('$value')"处,对$value进行HTML编码后,XSS攻击依旧存在;

<p onclick="updatecontent('hello world HTML编码,依旧存在XSS&#x27;&#x29;&#x3b;alert&#x28;1&#x29;&#x3b;eval&#x28;&#x27;')">Click setTimeOut HTMLEncode</p>

当你在onclick="updatecontent('$value')"处,对$value进行JavaScript编码后,XSS攻击依旧存在;(有兴趣的可以试一试)

<p onclick="updatecontent('hello world JavaScript编码,依旧存在XSS\x27\x29\x3balert\x282\x29\x3beval\x28\x27')">Click setTimeout JavaScriptEncode</p>

如果要想对$Value进行编码防范xss,需要对其进行doublejsencode,一次JavaScript编码是不够,需要2次,才能保证传入setTimeout中的参数url只是个字符串。如果仅仅进行一次javascript编码,能够保证updatecontent函数传入的是字符串,但是setTimeout会将该字符串当做代码执行,如果进行2次JS编码,在setTimeout接收到的参数是一次JS编码的值,只会对其进行一次JS解码,当做字符串处理。

<p onclick="updatecontent('hello world \x5cx27\x5cx29\x5cx3balert\x5cx282\x5cx29\x5cx3beval\x5cx28\x5cx27')">Click setTimeout Double JavaScriptEncode</p>

相关参考:

新式网络浏览器幕后揭秘:

www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#The_browser_main_functionality

XSS的原理分析与解剖:XSS的原理分析与解剖 - FreeBuf.COM

浏览器Lexer与XSS-HTML编码:浏览器Lexer与XSS-HTML编码 - FreeBuf.COM

XSS (Cross Site Scripting) Prevention Cheat Sheet:

www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet

推荐阅读更多精彩内容