CVE-2026-92938: vm2 3.11.3 through 3.11.6 Remote Code Execution via node:sqlite

Published Sep 17, 2026
·
Updated

Summary

vm2 3.11.6 exposes Node.js's host node:sqlite module to NodeVM code when that builtin is allowed explicitly or through builtin: ['']. The module is wrapped as read-only, but callable methods retain host-process authority. A sandboxed plugin can construct an in-memory database with extension loading enabled and call DatabaseSync.loadExtension() on a native library bundled in the plugin directory.

SQLite loads the library into the Node.js host process and invokes its native extension entry point. This gives the untrusted plugin arbitrary native code execution outside the sandbox.

The exploit needs only the node:sqlite builtin and a compatible native library already present in the untrusted plugin package. It does not require fs, process, module, childprocess, workerthreads, vm, inspector, vm2 nesting, or an existing database file.

Details

The vulnerable boundary spans the builtin inventory, resolver, runtime loader, and generic read-only wrapper.

On current Node.js versions, the builtin inventory contains the literal name node:sqlite. vm2 admits that name when it is explicitly configured or when the wildcard allowlist is expanded:

js const BUILTINMODULES = module.builtinModules.filter(/ denylist checks /);

The resolver then treats every request beginning with node: as a core-module request, even if the complete request string is not an allowlist key:

js if (x.startsWith('node:') || this.builtins.has(x)) { return x; }

The sandbox runtime removes exactly one node: prefix and looks up the remainder in the configured builtin map:

js if (filename.startsWith('node:')) { id = filename.slice(5); return loadBuiltinModule(id); }

Consequently, the sandbox spelling below resolves to the configured map entry node:sqlite:

js require('node:node:sqlite')

The default builtin loader imports the real module in the host realm and exposes it through vm.readonly():

js builtins.set(key, vm => vm.readonly(hostRequire(key)));

Read-only wrapping prevents property assignment. It does not remove dangerous callable capabilities. Calls to DatabaseSync and loadExtension() are forwarded to the host implementation.

The complete exploit flow is:

text attacker supplies an untrusted plugin package containing JavaScript and a native library -> host runs the JavaScript in NodeVM with node:sqlite allowed -> plugin resolves the host module as node:node:sqlite -> plugin creates an in-memory DatabaseSync with allowExtension enabled -> plugin derives the bundled library path from its own dirname -> plugin calls database.loadExtension(libraryPath) -> vm2 forwards the call to host SQLite -> SQLite loads the library into the Node.js host process -> SQLite invokes the library's native extension entry point -> attacker native code executes with the host process's privileges

The path does not need to be read through a sandboxed filesystem API. CommonJS already supplies the plugin's own directory, so an attacker can concatenate dirname with the known name of a bundled library. The host necessarily places the untrusted plugin package on disk before evaluating its JavaScript.

PoC

The attached PoC is local and harmless. Its native extension entry point writes one marker file and returns success. It does not launch a process, connect to a network service, or modify any other file.

Prerequisites: macOS, Node.js with node:sqlite, npm, and a C compiler.

1. Open the attached poc directory. 2. Install the exact affected package:

bash npm install --ignore-scripts

3. Compile and run the proof:

bash npm run poc

The script compiles extensionprobe.c as libvm2sqliteprobe.dylib, then runs untrusted-plugin.js in this restrictive configuration:

js new NodeVM({ console: 'off', require: { builtin: ['node:sqlite'] }, });

The sandboxed plugin performs only:

js const { DatabaseSync } = require('node:node:sqlite'); const database = new DatabaseSync(':memory:', { allowExtension: true }); database.loadExtension(dirname + '/libvm2sqliteprobe.dylib'); database.close();

A vulnerable result is:

json { "sandboxResult": { "nativeExtensionLoaded": true, "usedOnlySQLiteBuiltin": true }, "markerExists": true, "markerText": "VM2SQLITEEXTENSIONENTRYPOINTEXECUTED" }

The marker is written from the compiled native extension entry point, not from JavaScript. loadExtension() also returns successfully, proving that SQLite both loaded the library and invoked its entry point.

Impact

This is a sandbox escape to arbitrary native code execution. The native code runs inside the Node.js host process with the operating-system identity and privileges of that process, beyond all vm2 JavaScript, module, and proxy restrictions.

An attacker can replace the marker-only extension with native code that:

- reads application secrets, credentials, environment variables, and files available to the host account; - modifies application data or executable files and establishes persistence; - accesses internal services using the host's network identity; - steals other tenants' data from the same process; - terminates or corrupts the host process; and - performs any other operating-system action permitted to the host account.

The realistic affected workflow is a plugin platform, automation service, notebook, build service, or multi-tenant code runner that stores attacker-supplied package contents and evaluates the package's JavaScript in NodeVM while allowing node:sqlite or all builtins. The attacker does not need pre-existing host execution, a writable database, or a command-execution builtin.

Other sources

vm2 versions 3.11.3 through 3.11.6 expose Node.js's host node:sqlite module to code running in NodeVM when that builtin is permitted, either explicitly or through builtin: ['']. The module is wrapped with vm.readonly(), which prevents property assignment but leaves host-authority callables reachable; in addition, the resolver treats any request starting with 'node:' as a core-module request and the runtime strips only one 'node:' prefix, so a sandbox request for 'node:node:sqlite' resolves to the configured node:sqlite entry. Sandboxed code can therefore create an in-memory DatabaseSync with extension loading enabled and call DatabaseSync.loadExtension() on a native library bundled in the untrusted plugin package (path derived from dirname). SQLite loads the library into the Node.js host process and invokes its native entry point, giving the sandboxed plugin arbitrary native code execution outside the sandbox with the host process's privileges. The issue is fixed in vm2 3.11.7.

— MITRE

Affected Software

2 affected componentsFixes available
npm/vm2>=3.11.3<=3.11.6
npm/vm2>=3.11.3<=3.11.6
3.11.7

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/vm2 to a version that resolves this vulnerability.

    Fixed in 3.11.7
  2. Upgrade

    Upgrade vm2 to a version that resolves this vulnerability.

    Fixed in 3.11.7

Event History

Sep 17, 2026
CVE Published
via MITRE·01:46 PM
Data Sourced
via MITRE·01:46 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·02:17 PM
DescriptionSeverityWeakness
Oct 1, 2026
Advisory Published
via GitHub·03:29 PM
Data Sourced
via GitHub·03:29 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Deployments using vm2 3.11.3 through 3.11.6 are exposed when untrusted code runs in NodeVM and the node:sqlite builtin is permitted, either explicitly or through builtin: ['*']. The affected code can execute native code with the privileges of the Node.js host process.

2

What does an attacker need to exploit this issue?

An attacker needs the ability to run code in the affected NodeVM and have a native library bundled in their untrusted plugin package. They can use the package's __dirname-derived path to load that library through SQLite extension loading.

3

Are default builtin restrictions sufficient to prevent exploitation?

The issue depends on node:sqlite being allowed. Configurations that explicitly permit node:sqlite or allow all builtins with builtin: ['*'] are affected; the provided information does not establish exposure for configurations that do not permit that builtin.

4

How can I determine whether a deployment is affected?

Check whether vm2 is version 3.11.3 through 3.11.6, whether untrusted code is executed with NodeVM, and whether its builtin configuration permits node:sqlite directly or via builtin: ['*']. Also review whether untrusted plugins can include native libraries.

5

What should be done to remediate the issue?

Upgrade vm2 to version 3.11.7. Until upgrading, prevent NodeVM sandboxed code from accessing node:sqlite and avoid wildcard builtin permissions that include it.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203