Dropbox的Web安全防护策略之二:unsafe-inline指令和随机数配置

【编者的话】Dropbox的Web安全防护措施之一是使用基于内容的安全策略(CSP)。Dropbox的安全工程师Devdatta Akhawe通过四篇文章,介绍了CSP在Dropbox中推广的细节和经验。Dropbox的CSP原则大大减少了XSS和内容注入攻击。不过,大规模使用比较严苛的CSP规则将面临诸多挑战。我们希望通过这四篇CSP系列文章,将Dropbox在实践CSP过程中的收获分享给广大开发社区朋友。第一篇文章主要介绍如何在规则中设置报表筛选管线来标记错误;第二篇介绍Dropbox如何在上述规则中配置随机数及缓解unsafe-inline带来的安全风险;第三篇介绍如何降低unsafe-eval造成的风险,以及介绍Dropbox所开发的开源补丁;最后一篇介绍在权限分离机制下,如何减小第三方软件整合时的风险。本篇是该系列文章的第二篇,主要介绍Dropbox的随机数配置,还有unsafe-inline指令所带来的问题及解决方法。

在第一篇文章中,我们讨论了如何筛选错误报告及如何利用CSP为网站配置内容源白名单。通常而言,白名单中最重要的内容源是代码源,代码源由script-srcobject-src指令确定。标准的CSP配置一般含有一个受信任域名的列表,其中包括主网站名和可靠CDN等,它们由script-src及其他诸如unsafe-inlineunsafe-eval的指令给出。

这种规则阻止了随意注入的第三方脚本,但又由于有unsafe-inline这项指令存在,它并不能有效地防止XSS攻击。如果读者对unsafe-inlinenonce-src尚不熟悉,建议参阅这篇介绍文章。简而言之,CSP默认阻止所有内联脚本区块和内联事件(< div onclick="somescript" >),加强了代码和数据的分离。所有运行在网页中的代码必须来自白名单中的源所提供的脚本文件。这样便大大降低了XSS攻击的风险,但同时也需要巨大的移植工作量。

为了使移植变得简便,规则中允许一类特殊的源指令句法,它适用于script-srcstyle-src指令,如果含有匹配的随机数属性,那么则接受内联内容。这样,script-src源列表中含有nonce-randomnumber的规则就可以允许执行将“randomnumber”作为随机数属性值的脚本标记。随机数句法是CSP2的一部分,现在仅仅在Chrome和Firefox中支持。而在其他浏览器中,使用内联脚本标记需要用unsafe-inline使能所有内联脚本。

本篇文章将讨论我们在Dropbox网站上配置随机数的经验。在进入细节讨论之前,必须强调CSP只是一项缓解措施,并不能取代严格的验证及清理净化工作。Dropbox服务器和客户端分别使用的库是Pyxl和React,客户端的HTML清理使用的库则是DOMPurify。

配置脚本随机数


配置脚本随机数包含两个步骤:一、给所有内联脚本标记加上对应的随机数;二、移除所有内联事件处理器。Dropbox使用Pyxl生成服务器端的HTML。Pyxl将HTML标签转换为Python对象,在清理过程中自动去除非可信数据。我们调整了一些Pyxl清理代码,以便在脚本标记中插入正确的脚本随机数。

第二步移除内联事件处理器比较困难。有较长的一段时间我们极力反对使用内联事件处理器,Dropbox上新写的代码也没有它们,但这仍然造成了大量旧代码没有办法移植。为了减少过渡阶段的工作量,我们决定自动重写内联事件处理器来使用内联脚本标记。最基本的例子如下,原来的代码<div id="foo” onload="somescript">变为:


<div id="foo"><!-- if id is absent, we create a unique id -->
  <script> 
  // The nonce in the script tag above will be 
  // inserted during Pyxl serialization
  function(){
      var e = document.getElementById(foo);
      e.addEventListener("load", function(ev){
  //  ...somescript.. 
      });
  }();
  </script>

上述例子有几个值得关注的点。首先,我们使用了即时调用函数表达式,这样不会造成全局命名空间的混乱。第二,我们插入了一个脚本标记,它在原本的标记打开后加入了onload事件处理器。通常的加入操作是在原来的div元素结束或DOMContentLoaded事件之后,虽然它们也可能有很好的特性,但我们修改后的代码已经很接近浏览器原来的性能,因此这里我们使用的方法适用于自动转换。

读者们可能仍然会有疑问:以上转换没有在onload代码中修复已有的XSS攻击,所以并不是完全安全的转换。然而,我们认为这种危险是在可接受范围内,因为像Pyxl之类的系统,已经可以很好地识别并阻止在服务器的代码中的XSS。另外,随着时间的推移,我们仍将舍弃内联事件处理器,从而彻底规避这类风险。

通过上述的代码转换,只有在我们掌控中的内联脚本和事件处理器会执行。如果一次攻击尝试插入事件处理器(比如使用了DOMXSS等),支持随机数属性的浏览器将会阻止这类攻击。

内联脚本错误报告

和所有的CSP配置一致,推出一个新的随机数属性通常的做法是先在report-only模式下运行。Dropbox使用report-only模式将近一个月。同样也和内容源错误报告一样,内联脚本错误中的噪声滤除也十分重要。

除了上篇文章中提到的技巧,Chrome通过发送另外两项内容域(fields)来辅助识别内联脚本错误:脚本样本和源文件。脚本样本对于代码库中的快速查找十分关键,因为在本地重现问题较为困难。源文件则指的是通过DOM API插入内联脚本的JavaScript源文件。在筛选内联脚本错误阶段,我们过滤掉所有源文件域为URI的报告,这类错误报告不属于当前讨论的应用范围(广告插入和扩展工具是一类普遍的错误源)。类似地,参照Neil的建议,有些报告的脚本样本中包含的明显不是自己应用的代码(比如,脚本中包含字符串“lastPass”),我们也过滤掉了这一类错误报告。

Firefox有一个程序缺陷:即便nonce src允许执行内联脚本,Firefox仍会上报错误,同时也在执行代码。因此我们建议,为了减少噪声,在配置随机数时删除Firefox的report-uri

更新:这个程序缺陷已经修复了!不过修复的版本是2015年十月发布的Firefox 43,我们建议在2016年1月之前,不要为Firefox发送report-uri

利用合适的过滤器,我们给我们的规则配置了随机数源,而且开始查找错误并修复。对于像Dropbox这样巨大的规模和所拥有的代码库来说,这个过程十分枯燥。但是这项任务的回报值得我们做出努力,而且工作量也并非无法计算。经过几周的努力,我们成功地在执行模式下配置了nonce-src

无随机数支持下DOMXSS攻击的缓解

比较可惜的是,随机数源是CSP2中的功能。虽然Chrome和Firefox已经支持随机数源一段时间了,但Safari及Edge仍未支持。Safari和Edge都没有随机数源句法,却强化了unsafe-inline源表达式。为了使我们的网页应用性能更好,我们使用了另一个技巧:


document.addEventListener('DOMContentLoaded', function () {
    var metaTag = document.createElement('meta');
    metaTag.setAttribute('http-equiv', 'Content-Security-Policy');
    metaTag.setAttribute('content', "script-src https: 'unsafe-eval';");
    document.head.appendChild(metaTag);
});

简单地说,在浏览器执行所有HTML和同步脚本(包括内联脚本)后,激发DOMContentLoaded事件。在此之后,其他的JavaScript任务和事件进行排队,或者激发其他的onload处理器。随后的操作性能得到最大化,我们不再同时执行远程脚本,所以大部分JavaScript代码都在DOMContentLoaded之后执行。

上面展示的代码在DOMContentLoaded激发之后,插入了第二代CSP规则。需要特别说明的是,第二代CSP规则没有“unsafe-inline”。浏览器处理多个CSP规则时会强制遵循所有规则,只有符合全部规则的代码才能够得以执行。因此,在DOMContentLoaded事件激发之前,Safari和Edge仅在初始HTML中解析并支持内联脚本。DOMContentLoaded激发之后,这两个浏览器便停止支持内联事件处理器。

设想一下,在某次攻击插入了含有恶意有效载荷(<div onclick=alert(1)>)。Chrome和Firefox中的规则包含标头传递的随机数,可以阻止内联onclick。而在Safari和Edge中,第一个规则允许onclick处理器,DOMContentLoaded事件后插入的第二个规则阻止onclick。尽管这种保护相对于支持随机数源的浏览器中的保护措施较为薄弱,但它仍然阻止了大量的DOMXSS攻击。

最后的思考

上面提到的方法有效地缓解了Dropbox网页应用受到的XSS攻击。在Chrome、Firefox、Safari还有Edge等浏览器中,我们的大量用户受到的网页攻击也得到了适当地减轻。我们修复了所有注入漏洞,也为我们大多数用户修筑了二次防线以抵御更强的注入攻击。

配置像CSP这样重要的工程,尤其还要舍弃“unsafe-inline”,需要全公司的鼎力支持。感谢参与该项目的所有Dropbox成员,特别是安全工程团队的小伙伴们。这篇文章是实践CSP系列文章中的第二篇。下一篇,我们将介绍在规则中加入unsafe-eval的影响,以及如何它所带来的降低风险。

查看英文原文:[CSP] Unsafe-inline and nonce deployment

编后语

《他山之石》是InfoQ中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到[email protected]

你可能感兴趣的:(Dropbox的Web安全防护策略之二:unsafe-inline指令和随机数配置)