最近由于项目需要,在开发一系列浏览器插件,涉及的浏览器包括Chrome,Firefox和IE。在插件的实现中,部分功能需要通过跨域的API调用完成,这会导致一些问题,而问题在IE浏览器中尤为突出。
首先要说明的是,跨域访问是不可避免的。原因是我们开发的是插件而不是页面,API的调用都是在当前页面的环境中完成的,而我们的API服务器是自己内部的域名。这个问题在Chrome和Firefox的插件开发中不难处理,可以在相应的配置文件(如ChromeExtension的manifest.json)中允许某个域的访问即可,例如:
{
"name": "My extension",
...
"permissions": [
"http://*.google.com/"
],
...
}
我推断这两种浏览器插件允许跨域访问的原因是,插件中的脚本是在一套独立的脚本环境中执行的,而不是在当前页面的真实环境,因此即便通过跨域获得的资源有风险,也不会污染和危害当前页面的真实环境。(参见这里)相反,对于IE浏览器来说,没有类似的沙盒机制,插件中注入的脚本就是运行在当前页面的真实上下文中,API获得的资源直接可以控制当前页面的行为。因此IE浏览器为了安全考虑,对跨域访问的限制十分严格。
这里还有一个小问题,就是所谓限制跨域资源访问,到底保护的是谁的利益?这个问题应该从服务器和客户端两个方面理解。对于服务器,当收到一个请求时,会检查该请求来源,如果来源的客户端页面自己无法识别,而且服务器的数据又是比较敏感的,则可能做出限制,或者干脆拒绝访问(例如,黑客从他自己的site攻击你的服务器)。对于客户端,在请求前可能要检查这个请求的目标服务器是不是能够识别或者信任的,对于与当前页面所在域不同的服务器,浏览器可能会使用同源策略(same origin policy)进行限制甚至拒绝访问(例如,黑客通过让你访问他的服务器数据来攻击你的客户端页面)。在理解跨域访问的问题时,首先要清楚的事情就是此次访问的风险是来自哪里又作用到哪里。
我先从客户端说起。浏览器的同源策略可以限制对跨域资源的访问。在RFC标准中一个源(origin)的定义如下:
http://www.example.com:8080 (协议schema://域名domain:端口port)
因此,以上三部分只要有一个不同,浏览器就会认为是跨域资源访问。IE浏览器稍有不同,它的同源策略不会检查端口号。但是IE浏览器对协议不同产生的跨域请求的限制十分严格。以我开发时调试使用的IE 11浏览器(Windows 10)为例,如果从一个使用HTTPS协议的site访问一个HTTP协议的API(假设后者有权威机构颁发的数字证书),就会有问题,通常是报错:Access is denied。因为这是从安全的site访问不安全的site,IE考虑安全因素会abort该请求,对于ajax来说,是在XmlHttpRequest对象的open方法中抛出异常。如果使用HTTPS的API服务器没有提供证书,或者证书不可信任,则API调用会失败,这对所有浏览器都是如此。原因很好解释,没有证书的HTTPS站点可能被攻击者仿冒,后面会详细解释数字证书的设计动机和原理。以IE浏览器为例,发起ajax请求会报错:
XMLHttpRequest: Network Error0x2ee4, Could not complete the operation due to error 00002ee4.
一个workaround方法是,手动在客户端安装证书到浏览器的信任机构列表(Internet Options->Content->Certificates->Trusted Root CA),或者在浏览器地址栏访问一次api,然后在警告时选择信任该站点。但这不是解决问题的根本方法,最好还是申请一个权威机构颁发的证书(StartCom也提供了免费版的信任证书,不过只能针对外网的域名)。
上述证书的错误,并不是在所有版本的浏览器上都会出现。比如在Windows 8.1上的IE 11,在第一次请求时,浏览器会提示是否信任服务器的的证书,因为浏览器无法验证该证书。如果信任,则后续跨域API可以正常调用。其实数字证书存在的理由及其原理已经不属于跨域访问的范围了,但是考虑到从HTTP站点访问HTTPS站点仍是跨域访问,而HTTPS协议又离不开数字证书,这里就简单说一下我对证书的理解。
我们知道HTTPS协议实际上是在HTTP协议下层,在TCP协议上层,引入一个安全协议层,通常是SSL/TLS。目的是对要传输的数据进行一些处理,如加密解密,数字签名校验等,以保证传输数据的安全性和完整性。HTTPS使用对称加密算法对通信明文进行加密,而对称加密使用的密钥是在SSL握手阶段由服务器和客户端协商生成的,关于SSL握手的详细过程可以参考这篇文章,里面图文并茂,阐释详细明晰。第一个安全问题是:服务器客户端协商的密钥如何防止被窃取?SSL使用非对称的公钥加密算法对密钥进行机密后传输的。具体地,客户端通过通信两端产生的三个随机数生成一个密钥,并使用服务器的公钥进行加密,发送给服务器,服务器使用自己的私钥解密得到这个密钥。由于私钥只有服务器自己知道,因此密钥是安全的。第二个安全问题是:客户端如何知道他在与真正的目标服务器进行通信?攻击者很可能将自己的公钥发给客户端,然后冒充服务器进行通信。这就是数字证书存在的原因。数字证书就是由权威机构颁发的用来证明服务器身份的二进制文件。证书的内容包含了颁发机构、证书有效期、公钥加密和数字签名算法等信息。客户端拿到证书后,只要使用权威证书机构的公钥解密就得到证书内容。如果证书不是受信任的,则浏览器无法得到证书内容,即无法验证证书的合法性,就会发出警告。第三个安全问题是:虽然公钥加密是安全的,但是如何保证证书内容不被篡改呢?SSL使用数字签名机制保证数据的完整性。数字签名算法也是个非对称加密算法,服务器在证书中包含了数字签名算法的相关信息,由于服务器在对证书签名时使用了签名算法的私钥,因此在客户端使用签名算法的公钥就能得到明文内容的校验和,进而验证证书是否被篡改或仿冒。以上这些机制就保证了HTTPS通信的安全性。
有了这些原理的储备,就理解了HTTP通信的脆弱之处,就可以更好的理解为什么IE浏览器不允许从HTTPS站点访问HTTP协议的API。
对于服务器来说,每当遇到跨域资源访问的请求,需要与客户端进行沟通。这种沟通通过CORS(Cross-Origin Resource Sharing 跨源资源共享)来实现。CORS是W3C的一个草案,定义了必须访问跨源资源时,服务器和浏览器应该如何使用HTTPheader进行沟通,从而决定请求或响应是否成功。例如,浏览器发送一个简单的GET或POST请求,如果是跨域的,需要在请求中加入一个Origin header,用来告诉服务器请求的源信息:
Origin:http://www.example.com
服务器会根据这个header决定是否响应该请求,如果可以接受,则会在响应header中加入:
Access-Control-Allow-Origin:http://www.example.com
以上只是简单的请求,如果用户需要自定义header或者使用其他method,就不是附加header能搞定的了。这时需要一个Preflight请求与服务器沟通,该请求使用OPTIONS方法实现,通常会发送以下header:
Origin:与前面功能相同
Access-Control-Request-Method:请求使用的方法
Access-Control-Request-Headers:请求使用的自定义header
服务器收到Preflight请求后,依据允许的源、方法和header发送响应header,如:Access-Control-Allow-Methods,Access-Control-Allow-Origin,Access-Control-Allow-Headers等。这样客户端就知道服务器允许那些源、方和header了。由于Preflight会多发一次请求,因此浏览器会根据服务器返回的信息将Preflight响应缓存一段时间,用于后续的跨域请求。
常见的浏览器都通过XmlHttpRequest对象实现了CORS机制,而微软的IE 8引入了XDR(XDomainRequest)对象部分实现了CORS机制,但XDR从IE 11开始就不再支持。另外,IE 10以及之前版本不支持Preflight请求。据我的经验,IE 11会发送Preflight请求。
如果服务器不检查请求的来源会有什么安全问题呢?典型就是CSRF(Cross SiteRequest Forgery)攻击。顾名思义,攻击者会伪造客户端的请求对服务器进行攻击。(请参看:https://blogs.msdn.microsoft.com/ieinternals/2012/04/02/same-origin-policy-part-2-limited-write/)一个常见的例子是,我们使用电商网站购物时(如在亚马逊),我们的账户认证信息以及我们浏览商品的记录信息会存储在客户端(cookies),当我们点击某个商品详情时,会向电商的服务器发出HTTP请求,同时存储在浏览器的认证信息也会发送到服务器用于验证用户身份。服务器验证了你的身份后,会记录你当前访问的页面的信息,方便日后为你推荐一些商品。当你使用浏览器时,攻击者可能会诱导你点击某个站点的链接(如论坛中的某条评论),而这个链接的隐藏页面的后台会向你经常访问的电商网站发送一个HTTP请求(比如请求一个手机商品详情页面),由于你的账号信息记录在浏览器中,所以这次请求可以成功,而服务器也会相应记下你访问过这个页面。随后你浏览电商网站时,就可能看到那个手机商品的推荐,尽管你从未访问过那个商品。
服务器对跨域访问的检查就可以有效避免或限制这个行为。因为正常访问和伪造请求的区别就在于请求的源页面是否是服务器能够识别的页面。
以上就是我近期对跨域资源访问以及安全性的一点理解和小结。除了文中的链接,还参考了《HTTP权威指南》、《JavaScript高级程序设计》二书。