【漏洞预警】jQuery 前端库出现罕见的原型污染漏洞,影响范围广泛(含技术分析)
编译:奇安信代码卫士团队
2019年3月26日,距离上次 jQuery 安全漏洞披露已过去三年之久之时,Snyk 安全公司的研究员发现了这个开源前端库中的另外一个漏洞即原型污染漏洞 (CVE-2019-11358)。
jQuery 和原型污染漏洞简介
jQuery是一个快速、小巧、功能丰富的JavaScript库。它通过易于使用的API在大量浏览器中运行,使得HTML文档遍历和操作、事件处理、动画和Ajax变得更加简单。通过多功能性和可扩展性的结合,jQuery改变了数百万人编写JavaScript的方式,它应用于70%的互联网站点中。
jQuery 库上周修复的是罕见的原型污染漏洞。原型污染漏洞指的是攻击者修改 JavaScript对象原型的能力。JavaScript 对象就像变量一样,但存储的并非一个值 (var car = “Fiat”),而是能够包含基于预设结构的多个值(ar car ={type:"Fiat", model:"500", color:"white"})。
原型定义了 JavaScript 对象的默认结构和默认值,因此当未设置值时,应用程序不会崩溃。原型污染攻击可导致攻击者覆写 JavaScript 应用程序对象原型。由攻击者控制的属性可被注入对象,之后或经由触发 JavaScript 异常引发拒绝服务,或篡改该应用程序源代码从而强制执行攻击者注入的代码路径。
漏洞分析
1、重构易受攻击的应用程序
鉴于 jQuery 库的使用广泛见于前端。我们来看下原型污染漏洞在客户端应用程序中如何展现。
攻击始于用户输入,可导致恶意攻击者注入开发人员可能并未过滤或做任何特殊处理的对象。
例如,你正在构建一款应用程序,用户经授权能够发送和保存时一样的 JSON 有效负载。你授权用户该能力的原因可能是为了允许用户控制内容结构,而你不想承担该任务。
被发送的这类有效负载如下:
{
“myProperty” : “a”,
"__proto__" : { "isAdmin" : true }
}
我们假设你需要以递归方式克隆一个对象,但你对此毫无头绪。如果谷歌搜索“在 JavaScript 中输入克隆一个对象”,出现的首个 stackowerflow 搜索结果 (https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript)收获了4000多个星且被选为正确答案,而你很可能会复制粘贴这个答案并完成这项任务。
var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))
根据 stackoverflow 给出的建议,你认为这个代码示例会出现什么情况呢?
(1) myObject
展示的是字符串化格式,可能是从数据库字段中提取的。
(2) 使用JSON.parse()
函数和jQuery extend()
函数克隆myObject
拷贝。
(3) 新的克隆版本被称为newObject
。
当 JSON.parse()
处理如该示例中名为 _proto_
的属性时,你可能认为它会将属性 isAdmin
分配给该对象父属性的 true
。然而,实际上它创建了带有该属性名称的一个对象,从而覆盖了属性链功能。
当JSON.parse()
的这种行为结合不安全的深克隆能力时(或称为对象合并),它将分配给_proto_
属性的值泄露到 JavaScript 全局对象中。
有了这个铺垫后,我们再来看如下的代码片段及原型污染攻击的影响:
var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))
// if you do console.log({}.isAdmin) you’ll get true returned
// later down the application source code we may try to detect if the user is an admin or not
If (user.isAdmin === true) {
// do something like load the relevant interface, run an Ajax call, access localStorage, etc
}
如果从数据库中获取的用户对象并未在isAdmin
属性中设置任何值,那么用户对象本质上是未明确的。在这种情况下,访问 if 从句中的 isAdmin
属性将要求访问user
对象原型链中的父对象,即 Object
,它现在即被污染并且包含被值为 true
的 isAdmin
属性。最终,开发者的无意之举将导致用户被设置为管理员且能够对应用程序施加损害行为。
2、探索其它攻击向量
如上述案例中所示,不安全的递归合并操作,加上 JSON.parse
的运作方式将导致潜在的原型链污染后果。
然而,这并非修改该属性的唯一方法。假设如下代码:
let myObj = {}
myObj[‘__proto__’][‘a’] = ‘a’
console.log(myObj.a)
let newObj = {}
console.log(newObj.a)
在该示例中:
(1)创建了一个新的 myObj
。
(2)该属性链可通过 _proto_
访问,而该对象被修改为包含一个新的字符串属性。
由于该 myObj
属性实际上是我们修改的 JavaScript Object,此后创建的任何新对象也将包含该属性。产生这种情况的原因在于 JavaScript 的运作方式:如过 a
对象的属性(newObj
上)中并不存在属性,那么 JavaScript 访问 newObj
原型进行查找,并且在它耗尽整个原型链之前进行递归访问。在我们的案例中,该属性不存在,而这也是为何访问它会返回字符串值的原因。
你可能会纳闷:其他人如何能够以我们所述方式注入_proto_
对象访问呢?
让我们构建一款列出 npm数据包的应用程序并做出一些操作,如在用户界面上列出这些数据包。
我们可能需要一个能够发送 npm 数据包列表的 API 及其 package.json 文件内容:
[
{
"cool-package": {
"license": "MIT",
"github": "https://github.com/someuser/cool-package"
}
},
{
"__proto__": {
"license": "MIT",
"github": "https://github.com/",
“toString”: “april fools”
}
}
]
你可能认为这个 API 是安全的,因为它直接源自 npmjs 本身以及它们所提供的 API,或者源自其它受信任站点而攻击者并不拥有 API 服务本身,但你错了。你可能已经注意到,该数据包详情如名称以及 package.json
内容实际上是由用户控制的。
要在该样本应用程序场景上进行构建,我们假设开发人员想要从数组中构建映射,这样他们就能非常轻松地访问数据包,而无需遍历数组来提取数据。
该开发人员可能会尝试访问如下数据:
const pkgLicense = packageList[‘jquery’][‘license’]
此类代码示例如下:
let list_of_npm_packages = JSON.parse(`
[
{
"cool-package": {
"license": "MIT",
"github": "https://github.com/someuser/cool-package"
}
},
{
"__proto__": {
"toString": "april fools"
}
}
]
`);
list_of_npm_packages.forEach((package) => {
for (const [propertyKey, objectValue] of Object.entries(package)) {
for (const [name, value] of Object.entries(objectValue)) {
if (!packagesMap[propertyKey]) {
packagesMap[propertyKey] = {}
}
packagesMap[propertyKey][name] = value
}
}
})
如果我们要创建一个全新的对象并访问它们的 toString
属性,那么我们就会发现原型污染攻击。
const a = {}
console.log(a.toString) // prints ‘april fools’
console.log({}.toString) // prints ‘april fools’
3、如何避免原型污染攻击
要避免原型污染,可采用多种缓解措施和安全最佳实践:
(1) 确保使用安全的递归合并实现。
(2) 考虑在无需原型的情况下创建对象,如使用Object.create(null)
。
(3) 在处理用户控制的数据时,避免使用或不使用方括号表示法。考虑为基于映射的结构使用 Map 语言原语。
影响范围广泛
其实原型污染攻击早在多年前就已出现。不过由于 JavaScript 从处理基本的 UI 交互到和大量敏感数据交互发展成为服务器端语言后,它被单列为一类漏洞。因此任何一种原型污染攻击都能够在这个或多或少和 JavaScript 相关联的世界产生严重影响,桌面、移动、浏览器或服务器端应用程序等等不一而足。
在去年,研究人员开始更多地关注所使用的 JavaScript 库并查找潜在的原型污染漏洞且收获颇丰:Mogoose、lodash.merge、node.extend、deep-extend 和 HAPI 中均被发现原型污染漏洞。Snyk 公司表示已经发现了20多起原型污染攻击,“遍布浏览器和 Node.js 生态系统”。
不过好在原型污染漏洞并非可大规模被利用的漏洞,因为必须为每个目标单独设置利用代码。它要求攻击者深入了解每个网站和对象原型的运作方式以及这些原型如何在庞大的图式中进行分解。另外,某些站点并不使用 jQuery 进行重大操作,只是在做一些动画并零星地做一些弹出窗口等。另外,应用程序和网站如果使用的是闭源代码,也不受此类攻击影响。
然而,在 jQuery 被用于更复杂的操作中如构建完整的前端或和服务器端系统进行交互,那么原型污染攻击可导致黑客攻破系统,而这也是针对高价值网站的理想漏洞。
如下示例 PoC 展现了 jQuery 扩展 API 被用于多个对象的递归合并中。
let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode); // true
修复方案
更新至 jQuery 的最新版本 v3.4.0。
不过由于如今多数网站仍然使用 jQuery 库的 1.x 和 2.x 分支,也就是说大量基于 jQuery 的应用程序和网站仍然易受攻击。考虑到三个主要版本之间存在一些语法破坏,且 web 开发人员考虑的是修修补补而非重写前端,因此多数网站在可预见的未来必然会使用旧版本。幸运的是,该补丁已被向后兼容到老旧版本。
原文链接
https://snyk.io/blog/after-three-years-of-silence-a-new-jquery-prototype-pollution-vulnerability-emerges-once-again/
https://www.zdnet.com/article/popular-jquery-javascript-library-impacted-by-prototype-pollution-flaw/
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。