【编者的话】Dropbox的Web安全防护措施之一是使用基于内容的安全策略(CSP)。Dropbox的安全工程师Devdatta Akhawe通过四篇文章,介绍了CSP在Dropbox中推广的细节和经验。Dropbox的CSP原则大大降低了XSS和内容注入攻击的风险。不过,大规模使用比较严苛的CSP规则将面临诸多挑战。我们希望通过这四篇CSP系列文章,将Dropbox在实践CSP过程中的收获分享给广大开发社区的朋友们。第一篇文章主要介绍如何在规则中设置报表筛选管线来标记错误;第二篇介绍Dropbox如何在上述规则中配置随机数及缓解 unsafe-inline
带来的安全风险;第三篇介绍如何降低 unsafe-eval
造成的风险,以及介绍Dropbox所开发的开源补丁;最后一篇介绍在权限分离机制下,如何减小第三方软件接入时的风险。本篇是该系列文章的第四篇,主要介绍在权限分离机制下,如何减小第三方软件接入时的风险。
在本系列之前的几篇文章中,我们讨论了Dropbox配置CSP的经验,着重介绍了控制脚本源的script-src指令。通过锁定脚本源白名单、配置随机数源以及限制unsafe-eval风险等措施,Dropbox的CSP规则有效地缓解了XSS注入攻击。在所支持的浏览器中,只有来自于我们自己的网站、CDN或其他受信第三方的代码可以在Dropbox的网页应用中运行。
然而,配置CSP同时使用第三方接入又有其特有的风险与挑战。在本篇文章中,我们会讨论如何用 HTML5权限分离 的方法来解决此问题。
首先举一个例子来说明在我们网站上运行的第三方接入代码: 企业版Dropbox 中有一个 SnapEngage 提供的即时聊天小工具,根据网页应用与SnapEngage集成的机制,我们在HTML页的脚本节点中插入了以下代码。
var se = document.createElement('script'); se.type = 'text/javascript'; se.async = true; se.src = '//storage.googleapis.com/code.snapengage.com/js/' + chatId + '.js'; se.onload = se.onreadystatechange = function() { ... //elided var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(se, s);
上面的代码生成了指向SnapEngage库的脚本节点,同时设置了onload处理器,并将节点插入到页面中。载入的JavaScript库接下来生成显示聊天工具的标记和相关的事件处理器,并把它们插入到页面中。
这一过程存在着两个关键的问题。首先,聊天工具的标记和事件处理器并未使用CSP规则,因此,如果聊天工具使用了eval或内联事件处理器,我们的CSP规则便会阻止启动聊天工具。当我们第一次在Dropbox网站上为script-src配置随机数时,嵌入了聊天工具的页面不得不禁用CSP规则,这也就增加了页面遭受XSS攻击的风险。
另一个问题是一个不易察觉的威胁:该过程增加了Dropbox的 可信计算基 ,最终包含了SnapEngage的服务器。根据同源策略,所有运行在www.dropbox.com源上的代码拥有相同的权限。增加可信计算基实质上隐性地增加了安全风险。未来的 网络平台提升 方法可以降低这一类的安全风险,但该方法目前仍在万维网联盟(W3C)内部讨论。
我们很了解上述风险,Dropbox第三方接入的提供者也对自身进行了全面审查,并且强制执行了安全条约。但在实际中,仍然存在某些第三方提供者并未认真执行安全检查的情况,本着对用户负责的态度,我们认为此类数据安全风险过高无法接受,必须采取相应措施降低风险。
对此,一个可行的解决方案是,只将接入代码复制到我们的服务器中,同时修改聊天工具,使之去掉内联事件处理器。然而这种侵入式的解决方法代价巨大,更为糟糕的是,只要聊天工具升级更新,Dropbox就必须人工进行检查和提交代码。如果将这个步骤自动完成,那么同样会产生许多安全隐患。
我们使用 权限分离 的方法来解决第三方接入所带来的问题,其核心思想很简单:在无权限的源(origin)中运行第三方代码,避免了直接在Dropbox源中运行第三方代码。第三方代码通过iframe接入网站,iframe间的通信使用postMessage函数。Dropbox源提供更小的受信API,在不降低安全性的前提下保证了相关功能。
这同 OpenSSH 和 Google Chrome 的权限分离架构很类似,无权限的子进程与无权限的源通信,有权限的进程与有权限的(Dropbox)源通信。不过与库应用中使用的IPC机制不同,我们使用postMessage实现iframe间的通信。
具体来看一下SnapEngage接入到底怎样做到了权限分离。当载入一个含有SnapEngage聊天工具的网页时, https://www.dropbox.com 源中的代码生成一个指向 https://www.dbxsnapengage.com 的iframe。接下来,dbxsnapengage.com上的代码载入SnapEngage聊天工具。iframe自带的CSS隐藏聊天工具的边框,看起来聊天工具便和Dropbox网页天衣无缝地组合在了一起。
当然,这种方法的效率并不高。为了维护相关功能,聊天工具需要和主页集成。比方说,我们希望只在用户点击页面顶端的“Chat”按钮时,聊天工具才显示出来。之前的JavaScript代码通过监测“Chat”按钮上的按键动作,调用startSupportChat函数来实现该功能。
function startSupportChat() { SnapEngage.setWidgetId(SUPPORT_ID); SnapEngage.setUserEmail(chatData.Email, true) SnapEngage.startChat("How can we help you today?") }
startSupportChat函数会调用初始化聊天功能的相关SnapEngage代码。由于SnapEngage代码运行在dbxsnapengage.com上,这个函数并没有产生任何效果,也不存在于www.dropbox.com当中。我们对此的做法是,在www.dropbox.com上修改代码来给iframe发送消息。
DropboxSnapEngage.startSupportChat = function() { this.chatRequested = true; DropboxSnapEngage.showSnapEngageIframe(); return DropboxSnapEngage.sendMessage({ 'message_type': 'startSupportChat', 'chatData': this.chatData }); }; DropboxSnapEngage.sendMessage = function(data) { var content_window; content_window = DropboxSnapEngage.getSnapEngageIframe().contentWindow; return content_window.postMessage(data, this.SNAPENGAGE_IFRAME_ORIGIN); };
上述代码给dbxsnapengage.com的iframe发送消息。接着,dbxsnapengage.com通过定义下面的postMessage事件处理器来调用startSupportChat函数。
function receiveMessage(event) { if (!validOriginURL(event.origin)) return; var data = event.data switch (data.message_type) { //elided ... case "startSupportChat": startSupportChat(); break; //elided ... } }
最终的效果为,点击“Chat”按钮后将显示iframe,并给聊天工具代码发送消息以启动聊天功能。所有的聊天工具代码都运行在无Dropbox权限的dbxsnapengage.com源中。
以上所讲的仅仅是一个例子,我们在Dropbox网站中不同的位置都使用了权限分离的方法,有效地降低了第三方接入(例如,支付服务提供商的接入)所带来的风险。另外,作为第三方提供者的SnapEngage不需要做任何的改动,可以继续使用内联事件处理器。这个设计的特点虽然看起来微不足道,但事实上非常重要。我们拥有并运营着dbxsnapengage.com域名,也开发完成了跨权限的postMessage API。
查看英文原文: [CSP] Third Party Integrations and Privilege Separation
《他山之石》是InfoQ中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到editors@cn.infoq.com。