【编者的话】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-src
和 object-src
指令确定。标准的CSP配置一般含有一个受信任域名的列表,其中包括主网站名和可靠CDN等,它们由 script-src
及其他诸如 unsafe-inline
、 unsafe-eval
的指令给出。
这种规则阻止了随意注入的第三方脚本,但又由于有 unsafe-inline
这项指令存在,它并不能有效地防止XSS攻击。如果读者对 unsafe-inline
及 nonce-src
尚不熟悉,建议参阅这篇 介绍文章 。简而言之,CSP默认阻止所有内联脚本区块和内联事件( < div onclick="somescript" >
),加强了代码和数据的分离。所有运行在网页中的代码必须来自白名单中的源所提供的脚本文件。这样便大大降低了XSS攻击的风险,但同时也需要巨大的移植工作量。
为了使移植变得简便,规则中允许一类特殊的源指令句法,它适用于 script-src
和 style-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
。
比较可惜的是,随机数源是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中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到editors@cn.infoq.com。