【编者的话】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中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到editors@cn.infoq.com。