A learning platform that lets students compose MongoDB queries has dropped Node.js’s vm module and now parses every query into an abstract syntax tree (AST) before execution. The change removes a sandbox that could be broken, protecting the backend from arbitrary code that a malformed string could inject.

Why the original sandbox failed

The first implementation wrapped a live database handle in a vm.runInContext call and tried to restrict users with a regular expression. It allowed only method names such as find or aggregate; every other identifier was supposed to be filtered out.

Two flaws made that approach insecure:

  • Regex filtering is bypassable. JavaScript lets code use bracket notation (obj["constructor"]) to reach any property. An attacker can retrieve the Function constructor, build a new function, and run any code they choose. The regex never sees the underlying property access because the source can be rewritten in countless ways.
  • vm is not a security boundary. Node’s documentation states that vm isolates the global object but not the whole process. By injecting a live database connection into the sandbox, the code inside the context retained the ability to call any method on that connection, including ones that write or delete data. The sandbox did not stop the code from affecting the host process.

The AST-based solution

The team replaced code execution with static analysis. Query strings now feed the acorn parser, which produces an AST—a tree representation of the code’s syntactic structure. The AST is examined node-by-node against a strict whitelist:

  • Literals, arrays and objects are allowed only when they appear as plain values.
  • Method calls are limited to a predefined set (find, sort, limit, etc.). Any other call is rejected.
  • Computed property access (e.g., obj[expr]) or any node type not explicitly listed triggers an immediate failure.

Because the parser works on the tree, not the raw text, it cannot be fooled by alternate spellings or bracket-notation tricks. A constructor chain that would have slipped past the regex appears as an unrecognised node and is rejected before any code runs.

What this means for security

The new design follows a “deny by default” philosophy:

  • Define what is permitted, not what is forbidden. Textual allow-listing cannot be exhaustive; an AST has a finite set of node types, making exhaustive checking feasible.
  • Never expose live resources inside a sandbox. Passing a database handle into an isolated context gives sandboxed code a direct line to the backend. The parser approach never hands a live object to user code; it only extracts the intent of the query.
  • Validate structure, then execute safely. Once the AST passes validation, the platform translates the allowed calls into actual MongoDB driver methods using its own trusted code path.

What to watch next

  • Audit any use of eval, new Function, or vm in your codebase. Even a whitelist can be subverted by JavaScript’s dynamic nature.
  • Adopt AST parsing for user-generated code wherever possible. Libraries like acorn, esprima, or babel-parser make the transformation straightforward.
  • Limit exposure of live objects. If a database connection, file handle, or network socket must be reachable, wrap it in a proxy that only exposes the minimal methods you intend to allow.
  • Automate testing of edge cases. Generate queries that use bracket notation, computed keys, or prototype manipulation to verify your parser rejects them.

Takeaway

Relying on vm.runInContext as a sandbox gives a false sense of security; regex filters cannot cover JavaScript’s flexible syntax, and the sandbox does not isolate process resources. Parsing user input into an AST and whitelisting only the nodes you understand provides a concrete, maintainable barrier that stops malicious code before it runs. If your platform lets users write code, replace eval-style execution with static analysis today.