一个允许学生编写 MongoDB 查询的在线学习平台弃用了 Node.js 的 vm 模块,现在在执行前会将每个查询解析为抽象语法树 (AST)。这一改动移除了一个可能被攻破的沙箱,从而保护后端免受恶意构造的字符串注入任意代码的攻击。

为什么原始沙箱会失效

最初的实现方案是在 vm.runInContext 调用中封装了一个活动的数据库句柄,并尝试使用正则表达式来限制用户。它仅允许 findaggregate 等方法名;其他所有标识符都应该被过滤掉。

两个缺陷导致该方法并不安全:

  • 正则表达式过滤可以被绕过。 JavaScript 允许代码使用方括号表示法 (obj["constructor"]) 来访问任何属性。攻击者可以获取 Function 构造函数,构建一个新函数,并运行他们选择的任何代码。由于源代码可以以无数种方式重写,正则表达式永远无法检测到底层的属性访问。
  • vm 并不是安全边界。 Node.js 的文档指出,vm 隔离的是全局对象,而不是整个进程。通过将活动的数据库连接注入沙箱,上下文中的代码保留了调用该连接上任何方法的能力,包括写入或删除数据的操作。沙箱无法阻止代码影响宿主进程。

基于 AST 的解决方案

团队用静态分析取代了代码执行。查询字符串现在会输入到 acorn 解析器中,解析器会生成一个 AST——即代码语法结构的树状表示。AST 会逐个节点地根据严格的白名单进行检查:

  • 字面量、数组和对象 仅在作为纯值出现时才被允许。
  • 方法调用 被限制在预定义的集合中(如 findsortlimit 等)。任何其他调用都会被拒绝。
  • 计算属性访问(例如 obj[expr])或任何未明确列出的节点类型都会触发立即失败。

由于解析器是基于树而不是原始文本工作的,因此它不会被交替拼写或方括号表示法的技巧所欺骗。原本可能绕过正则表达式的构造函数链会表现为一个无法识别的节点,并在任何代码运行之前被拒绝。

这对安全性意味着什么

新设计遵循“默认拒绝”的理念:

  • 定义允许的操作,而不是禁止的操作。 文本形式的白名单无法做到面面俱到;而 AST 只有有限的节点类型,这使得详尽的检查变得可行。
  • 绝不要在沙箱内暴露活动资源。 将数据库句柄传递到隔离的上下文中,会给沙箱代码提供一条通往后端的直接路径。解析器方法永远不会将活动对象交给用户代码;它只提取查询的意图。
  • 验证结构,然后安全执行。 一旦 AST 通过验证,平台就会使用其自身的受信任代码路径,将允许的调用转换为实际的 MongoDB 驱动程序方法。

后续注意事项

  • 审计代码库中任何对 evalnew Functionvm 的使用。 即使是白名单也可能被 JavaScript 的动态特性所破坏。
  • 尽可能对用户生成的代码采用 AST 解析。 使用 acornesprimababel-parser 等库可以使这种转换变得简单直接。
  • 限制活动对象的暴露。 如果必须让数据库连接、文件句柄或网络套接字可访问,请将其封装在代理 (proxy) 中,仅暴露你打算允许的最小方法集。
  • 自动化边缘情况测试。 生成使用方括号表示法、计算键或原型操作的查询,以验证你的解析器是否会拒绝它们。

总结

依赖 vm.runInContext 作为沙箱会给人一种虚假的安全感;正则表达式过滤器无法覆盖 JavaScript 灵活的语法,且沙箱无法隔离进程资源。将用户输入解析为 AST 并仅对你理解的节点进行白名单处理,提供了一个具体的、可维护的屏障,可以在恶意代码运行前将其拦截。如果你的平台允许用户编写代码,请立即用静态分析取代 eval 式的执行。