【编者的话】Dropbox 的 Web 安全防护措施之一是使用基于内容的安全策略(CSP)。Dropbox 的安全工程师 Devdatta Akhawe 通过四篇文章,介绍了 CSP 在 Dropbox 中推广的细节和经验。Dropbox 的 CSP 原则大大减少了 XSS 和内容注入攻击。不过,大规模使用比较严苛的 CSP 规则将面临诸多挑战。我们希望通过这四篇 CSP 系列文章,将 Dropbox 在实践 CSP 过程中的收获分享给广大开发社区朋友。第一篇文章主要介绍如何在规则中设置报表筛选管线来标记错误;第二篇介绍 Dropbox 如何在上述规则中配置随机数及缓解unsafe-inline
带来的安全风险;第三篇介绍如何降低unsafe-eval
造成的风险,以及介绍 Dropbox 所开发的开源补丁;最后一篇介绍在权限分离机制下,如何减小第三方软件整合时的风险。本篇是该系列文章的第三篇,主要讨论如何降低 CSP 中的 unsafe-eval 指令所带来的风险,以及介绍 Dropbox 为此而开发的开源补丁。
在之前的两篇文章中,我们讨论了 Dropbox 如何大规模配置 CSP 来防止注入攻击。第一篇文章主要讨论的是如何筛选错误报告,获得噪声较小的白名单,从而限制应用中运行的代码源;第二篇文章主要讨论,随机数源如何缓解内容注入带来的XSS 攻击。尽管如此,CSP 规则中的另一个关键字 unsafe-eval
允许字符串转代码的用法(比如,eval
、new Function
、setTimeout
等),这又留下了 XSS 攻击的隐患。
显然,上面这个问题必须解决。但由于旧式JS 模板在Dropbox 客户端代码中的大量使用,全面禁止eval 并不容易。在我们用React 替换旧式模板的过程中,我们也在思考 unsafe-eval
造成危险的确切机理和解决方法。
unsafe-eval
乍看起来并不像致命的不安全指令。unsafe-eval
只决定浏览器是否允许 eval,其变量类似于new Function
。但是,如果一次攻击中可以调用 eval,那么这次攻击就已经到达了代码执行的程度,这必会造成损失。与unsafe-inline
不同,在unsafe-eval
造成的漏洞中,攻击插入字符串并随字符串进入 eval“槽”,而unsafe-inline
则允许攻击者将简单的 HTML 注入漏洞转变为代码注入漏洞。
不幸的是,在更深入的探索中我们意识到,上述解释并不正确。产生攻击漏洞的主要原因是我们使用了 jQuery、Prototype 之类的库。事实上,使用 jQuery、Prototype 时,unsafe-eval
抵消了移除unsafe-inline
所带来的优势。我们会深入讨论 jQuery,类似的问题也同样存在于 Prototype 或其他库中。
请看以下两行 HTML 代码,似乎它们的运行结果相同:
document.getElementById("notify").innerHTML = untrusted_input jQuery("#notify").html(untrusted_input)
不允许内联脚本的 CSP 规则中,untrusted_input
可能含有全局中所有的onclicks
,浏览器不会执行它们。这一点对于两行代码都适用。但是untrusted_input
包含一个内联脚本标记(如,alert(1)
),这就使得两行代码大不相同了。
在第一行代码中,innerHTML
不支持内联脚本标记,alert
不会执行。而在第二行代码中,jQuery 将解析脚本标记,直接用innerHTML
设置untrusted_input
并不会起作用。jQuery 会解析脚本标记,并直接在脚本标记中 eval 代码。更糟糕的是,如果untrusted_input
是 https://attacker.com/foo.js ,那么 jQuery 会 XHR 注入那个 foo.js 文件并 eval 它,内容源对脚本的限制甚至会失效。完成这一动作的代码在 jQuery 核心的 domManip 函数中。jQuery 代码在几乎所有 DOM 操作(插入、追加、html 等)中,都会调用该函数。
此类问题的另一个例子是jQuery.ajax
函数。这个函数看起来是一个普通的用来产生 XHR 请求的函数,但 jQuery 从设计上赋予了它的 ajax 函数更多功能。特别当 XHR 请求的应答中包含内容型脚本时,jQuery 会 eval 应答(参见 GitHub 讨论)。这意味着,只要是攻击者可以控制目标 ajax URI 的地方,都将成为代码注入漏洞。
不允许 eval 的 CSP 规则中,浏览器会阻止上述的漏洞。但实施这种 CSP 规则代价巨大。为了减少此类风险,我们开发了一项 jQuery 顶层“安全补丁”,以防止非安全操作。我们很乐意将我们的 jQuery 补丁开源来帮助解决以上“意外的 eval 操作”,希望广大的社区开发者们可以从中受益。如果开发者朋友们发现其他地方需要打补丁,也请和我们分享!
补丁中有两个重要的组成部分。首先,通过添加以下代码,移除了 ajax 中的隐式 eval。这行代码使用一个 no-op 代替了脚本应答的默认处理器(放置在 jQuery 代码使用 eval 的地方)。
jQuery.ajaxSettings.converters["text script"] = true
第二,重写了默认的domManip
函数,在执行前检测脚本标记及随机数的正确性。补丁仅仅重新实现了domManip
函数(完全从 jQuery 中复制出来),不过补丁的关键之处在函数的第 183 行:
// line 181: for (i = 0; i < hasScripts; i++) { node = scripts[i]; if ((window.CSP_SCRIPT_NONCE != null) && (window.CSP_SCRIPT_NONCE !== node.getAttribute('nonce')) { console.error("Refused to execute script because CSP_SCRIPT_NONCE" + " is defined and the nonce doesn't match."); continue; }
另外一种解决方案是完全删除可能造成误操作的代码,或使用 jPurify 等插件清理 jQuery 所有的 DOM 操作。但文章这里的重点是,如果配置的 CSP 规则允许unsafe-eval
,那么减小 XSS 攻击风险的措施便十分重要。
可信任 eval 的使用
正如之前所提到的那样,因为我们早先的代码仍在使用不安全的 eval,我们无法完全移除规则中的 unsafe-eval。特别地,当使用 JavaScript Microtemplates 时,我们还需要 unsafe-eval。本质上,该模板库使用new Function
来对“text/template
”内容型脚本标记中的模板进行 eval 操作。例如下面这个模板
<script type="text/html" id="user_tmpl"> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </script>
模板代码使用 id 参数查找模板,然后在上面的脚本标记中调用new Function
函数,但这样也使得攻击者可以用 HTML 注入漏洞插入恶意模板。这里的模板是由我们的模板库 eval 所得。
为了解决这个问题,我们在所有模板脚本标记中插入随机数属性,并修正了模板库,检查模板节点的随机数属性。这类似于浏览器检查脚本节点的随机数属性。
<script id=test type=text/template nonce=1234> ...// template library only processes this if ...// window.CSP_SCRIPT_NONCE equals 1234 </script> <script type=text/template> ...//the templating library will ignore this </script>
我们遇到的另一个问题是,有时客户端代码会在网页载入之后下载模板。由于服务器每次生成一个新的随机数,网页载入后下载下来的模板中的随机数会和主页中的随机数不一样。我们通过修改服务器端的代码解决了这个问题。以前每次载入都要产生随机数,现在替代的方法是,网页的脚本随机数是 CSRF 令牌的 hash 值(CSRF 令牌已经是一个不可预测的随机值了)。这个方法将随机数安全性简化为 CSRF 令牌安全性。不过,如果攻击者知道使用的是 CSRF 令牌,可能对用户进行 CSRF 攻击。
最后再一次提醒读者,CSP 是一个缓解风险的措施,属于深度防御,并不是网页应用安全的第一道防线。最适合 XSS 的防御方法是安全搭建 HTML,使用的框架应当能够自动规避非信任数据,同时使用性能较好的 DOM 清理器作为第二道防线。
在下一篇文章中,我们将讨论 CSP 和第三方软件整合的问题,及其相关的风险。
查看英文原文: [CSP] The Unexpected Eval
编后语
《他山之石》是 InfoQ 中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到 editors@cn.infoq.com。
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。
评论