【编者的话】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,我们无法完全移除规则中的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中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到[email protected]。