🔥 CyberForge CVE of the Day #056
🚨 CVE-2026-93606 — vm2: a missing Promise handler opens a path out of the sandbox
The callback sanitiser was waiting for a callback. The attacker left that callback out and JavaScript’s own rejection machinery carried the value onward.
In affected vm2 deployments, host-Promise construction and rejection handling can combine to deliver powerful host objects to untrusted guest code. A public demonstration shows host command execution and access to host-only environment data under the supplied harness conditions.
CVSS v4.0 and v3.1: 10.0 Critical. Fixed in vm2 3.12.1.
One date matters immediately: 18 September is the CVE publication date. The advisory and fix were already public on 3 September.
⚠️ Prioritise deployments that run untrusted scripts and expose host asynchronous APIs. The actual Promise and its rejection values determine the practical escape path.
🎯 The quick hit:
🔹 Affected: npm vm2 through 3.12.0, including VM and NodeVM.
🔹 Failure: species manipulation and an omitted rejection handler bypass the intended host-value sanitiser.
🔹 RCE prerequisite: an exposed host-realm Promise whose rejection value supplies useful host authority.
🔹 First action: deploy 3.12.1 or a verified backport, replace affected workers and review host API failure paths.
🔹 Evidence: public PoC; malicious exploitation not confirmed in this review.
🔑 Key details:
⭐ Severity: Critical
📊 CVSS v3.1: 10.0; CNA CVSS v4.0 also 10.0
🧮 Vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`
🧠 Weakness: CWE-693 — Protection Mechanism Failure; project also lists CWE-913
🎯 Targets: vm2 VM and NodeVM through 3.12.0
🧩 Vulnerable boundary: Host Promise settlement → guest-controlled result capability
🌐 Attack vector: Network in the published assessment; delivery depends on the embedding application
⚙️ Attack complexity: Low in the published vector
🔓 Privileges required: None in CVSS; application access requirements still apply
👆 User interaction: None
🔀 Scope: Changed in CVSS v3.1
⚔️ Confirmed impact: Publicly demonstrated sandbox escape and host-process RCE under the advisory’s conditions
💥 CVSS impact: High confidentiality, integrity and availability
👤 Execution context: Node.js process hosting vm2; actual OS privileges vary
🛡️ Fix: vm2 3.12.1
🔑 Additional remediation: Audit exposed host APIs; investigate and recover where compromise warrants it
🚨 Active exploitation: Not confirmed in reviewed evidence
🧪 Reproduction: Public project-advisory PoC; not executed by CyberForge
📋 CISA KEV: No entry in retrieved 18 September catalogue; checked 19 September
📉 EPSS: FIRST returned no row; unavailable, not zero
🗓️ Earliest confirmed attacks: Unknown
🩹 Vendor patch: 3.12.1, released 3 September 2026
📜 Vendor bulletin: GHSA-6454-5x88-m6jw
🏷️ Researcher name: Not established from the reviewed advisory metadata
🔬 Campaign research: No confirmed campaign identified
The CVSS score does not create an exposed listener or remove authentication from a separate script-submission service. Map the real application entry point before describing remote reachability.
🧬 What actually went wrong?
vm2 lets a host application expose selected capabilities to guest JavaScript. When an exposed host API returns a Promise, the bridge must prevent that Promise’s results and failures from leaking authority outside the permitted interface.
Two protections missed the same path: species hardening applied to sandbox-local Promises, while rejection sanitisation expected a callable handler to wrap. A host Promise with an omitted handler followed different machinery.
1️⃣ The object belongs to the host realm
A realm has its own intrinsic objects and built-in functions. A host-created Promise retains host Promise behaviour. Installing guards on the sandbox’s own Promise prototype does not automatically govern it.
2️⃣ Species influences the next result
Promise chaining needs a result object and functions that settle it. Constructor/species selection can influence how that result capability is created. Here, the guest could steer that choice toward a recipient it controlled.
3️⃣ No callback means no callback wrapper
The bridge’s rejection protection wraps callable slots. When the handler is omitted or non-callable, JavaScript still propagates the rejection using its default behaviour—called the internal Thrower path in the advisory.
4️⃣ A host value arrives through the uncovered route
That propagation can deliver an unsanitised host rejection value to the guest-controlled result capability. If the value carries sufficient host authority, the guest can move beyond its intended sandbox permissions.
This is normal language behaviour interacting with incomplete mediation. The evidence does not establish a new V8 memory-corruption vulnerability.
🔬 The crucial distinction: sanitiser bypass versus host RCE.
The public harness deliberately exposes a host API whose Promise rejects with a powerful host object. Its checks demonstrate host-only environment access and host command execution.
That is meaningful proof under the supplied conditions. It does not mean an ordinary string rejection spontaneously becomes the host process object.
For a real deployment, review both the Promise crossing the boundary and its possible rejection values. Nested error details may retain privileged references even when the top-level object looks like an ordinary error.
⚔️ The practical attack chain:
1️⃣ An application accepts attacker-controlled JavaScript for vm2 execution.
2️⃣ An exposed host API returns a host-realm Promise accessible to that script.
3️⃣ Guest-controlled constructor/species behaviour influences the next result capability.
4️⃣ An omitted rejection handler leaves no guest callback for the bridge to sanitise.
5️⃣ Default rejection propagation delivers the host value through the attacker-selected result path.
6️⃣ A sufficiently powerful value enables a host pivot and execution with the embedding process’s authority.
The project advisory and CNA support this mechanism and its prerequisites. Internet delivery, account access and subsequent intrusion activity depend on the surrounding application; they are not universal properties of the npm package.
👤 Execution context — what the attacker actually gets.
“Host” means the Node.js process embedding vm2. It does not inherently mean root, SYSTEM, the container host or the physical machine.
Inspect the worker’s actual identity and accessible resources:
🔹 Environment variables and mounted secrets.
🔹 Writable application, temporary and shared paths.
🔹 Service credentials and available network routes.
🔹 Process-launch permissions and OS confinement.
These define the potential consequences if an escape succeeds. A low-privilege worker with narrow access has a different blast radius from a worker holding production credentials and broad internal connectivity.
Not established: automatic container escape, privilege escalation, credential theft, persistence or lateral movement. Those require their own evidence.
📦 Affected products and versions:
🔴 npm vm2: through 3.12.0
🟢 First fixed upstream version: 3.12.1
Check direct and transitive dependencies, bundled copies, custom forks and different worker images. An application may load more than one revision across its services.
A vendor backport needs explicit confirmation. Conversely, a newer package on disk does not prove an already-running worker has loaded the new code.
🛠️ Why the patch closes the missing-handler route.
The relevant security commit changes `lib/bridge.js` and adds advisory-specific regression coverage.
🔹 Find the effective operation. The bridge identifies the actual host Promise method and receiver, including indirect invocation and finally.
🔹 Constrain result construction. It temporarily shadows the host Promise’s constructor with an inert undefined value, forcing native machinery to use a genuine host Promise for the result.
🔹 Keep the existing callback defence. Callback wrapping remains, but result construction is now protected even when there is no callback to wrap.
🔹 Restore state after the call. A finally block restores the original property state.
🔹 Fail closed when necessary. Unsafe states that prevent the guard are rejected. An already inert, non-writable undefined constructor is a specific accepted case; not every non-configurable property is treated identically.
The fix also covers host Reflect.apply indirection so an alternative invocation route cannot simply bypass recognition of the Promise call.
The important design change: enforcement moves to a point that exists regardless of whether the guest supplies a rejection callback.
🕰️ Disclosure and exposure timeline:
🟢 3 September, 15:21 UTC: GitHub publishes the fixed 3.12.1 release.
🔵 3 September, 15:23 UTC: The project advisory becomes public.
🟢 3 September, 15:24 UTC: npm publishes 3.12.1.
🔵 18 September: CVE publication and NVD ingestion.
🔎 19 September: CyberForge review.
Defender consequence: do not start the exposure review only when your scanner received the CVE on 18 September. Assess earlier untrusted jobs while vulnerable code and the relevant host API were present. Disclosure dates are not proof of attack dates.
👁️ Defender hunting guide:
These are tailored investigation ideas, not validated detection signatures. No authoritative campaign IOC set was established.
1️⃣ Tie each guest job to its worker
Retain author or tenant, submission time, source hash, worker identity, vm2 revision and exposed API profile. Correlate that record with host telemetry. Without job-to-worker attribution, suspicious Node.js activity can be difficult to connect to the initiating script.
2️⃣ Review asynchronous failure paths
Audit host API rejection values for privileged references, including nested details. Review unusual constructor/species manipulation around exposed host Promises. These constructs can be legitimate or disguised, so keyword matching alone is insufficient.
Do not log complete sensitive objects into production telemetry merely to inspect them; use controlled review and narrowly scoped instrumentation.
3️⃣ Hunt for authority gained
Investigate unexpected Node.js child processes, executable files, service changes, secret access and outbound connections associated with guest jobs. Preserve user, parent process, timestamps, paths and connection metadata.
An escape can use host APIs in-process. No spawned shell does not mean no compromise.
4️⃣ Interpret new guard errors in context
After patching, errors involving unsafe Promise constructors or non-extensible host Promises may indicate blocked boundary conditions or a legitimate compatibility issue. Correlate the error with the source job and exposed API rather than classifying it alone.
5️⃣ Keep suspicious scripts as evidence
Preserve source, relevant logs, image digests and dependency manifests. Do not replay an untrusted sample through a production evaluator to see what it does.
🩹 Emergency remediation order:
1️⃣ Inventory and contain the reachable path
Identify script authors, host APIs, Promise origins and worker authority. If patching must wait, suspend untrusted execution or remove the relevant exposed API where operationally possible.
2️⃣ Deploy 3.12.1 or a verified backport
Update dependencies, lockfiles and built images together, then replace affected workers. Include replicas, queue consumers and recovery copies.
3️⃣ Verify the deployed dependency
From the application’s actual dependency context, read-only inventory commands include `npm ls vm2 --all` and `npm explain vm2`. Use equivalent tooling for other package managers.
These identify dependency paths; separately verify the copy loaded by the active service and any bundled artefacts. Record deployment and worker-replacement times.
4️⃣ Check legitimate compatibility
The release also restricts child_process builtin access and crypto.setFips, and addresses a separate host-global leak. Test intended scripts in staging. Do not restore powerful capabilities merely to suppress a new failure without reviewing why access changed.
5️⃣ Recover when evidence warrants it
Isolate suspected workers, preserve evidence, rebuild from trusted images and revoke secrets reachable to the compromised context. Track incident closure separately from dependency-update completion.
🧱 Temporary exposure reduction.
✅ Pause untrusted scripting or remove the relevant host API during an unavoidable update delay.
✅ Expose narrow, data-oriented interfaces and review successful results plus errors.
✅ Reduce worker credentials, filesystem permissions and network access.
✅ Separate trust levels using an appropriate operating-system isolation boundary.
A manually added catch handler is not an established comprehensive repair for guest-controlled chaining. The public demonstration also uses disabled eval and WebAssembly settings, so those switches should not be presented as this flaw’s fix.
These measures support deployment and limit consequences; they do not replace the package correction.
🚑 When vulnerability management becomes incident response.
Escalate when guest jobs correlate with unexpected host process activity, privileged file access, unauthorised changes or unexplained network connections. Investigate cross-tenant effects where workers or resources are shared.
A vulnerable dependency alone does not prove exploitation. Equally, an updated dependency does not remove persistence or invalidate secrets already obtained.
Scope recovery from the authority available to the worker and the evidence retained. Avoid claiming whole-host takeover or data theft solely from a sandbox escape alert.
📊 CISA KEV and EPSS context.
At review on 19 September 2026, the retrieved CISA catalogue was dated 18 September, 19:00:05 UTC and contained no entry for this CVE.
FIRST returned a successful response with no EPSS row. That means no available score, not zero exploitation probability.
CISA’s ADP enrichment records exploitation as poc, automatable as yes and technical impact as total. PoC-level evidence is not a confirmation of active malicious use.
The public demonstration and reachable host authority are sufficient reasons to prioritise remediation without waiting for a prediction score or KEV listing.
🧾 Evidence separation.
Vendor/project-confirmed: affected package and versions, Promise-boundary mechanism, public demonstration and a released patch.
Demonstrated under the published harness: host-only environment access and host command execution using a powerful rejection value. CyberForge did not run that harness.
CyberForge interpretation: audit asynchronous error paths, verify every active worker and constrain host authority independently of the JavaScript sandbox.
Not established: confirmed malicious exploitation, actor attribution, victim count, a campaign IOC set, universal root access or container escape.
🔥 CyberForge verdict:
CVE-2026-93606 is a critical boundary failure where a host Promise and a useful rejection value meet attacker-controlled guest code. Its value as a teaching case lies in the missing path: the sanitiser can be present and still never run.
The correct order is:
1️⃣ Map the reachable host interface and preserve suspicious evidence.
2️⃣ Deploy the fixed package and replace active workers.
3️⃣ Verify behaviour and hunt correlated host activity.
4️⃣ Recover and rotate affected secrets where compromise warrants it.
5️⃣ Review the authority available to future guest jobs.
When the error path can deliver authority, it is part of the security boundary.
🔗 Project advisory and public demonstration:
https://github.com/patriksimek/vm2/security/advisories/GHSA-6454-5x88-m6jw
🔗 Fixed release and upgrade notes:
https://github.com/patriksimek/vm2/releases/tag/v3.12.1
🔗 Security patch:
https://github.com/patriksimek/vm2/commit/1cbbf3d2d17f3336cc0480b9157fe92b2b1d2f16
🔗 Official CVE record:
https://www.cve.org/CVERecord?id=CVE-2026-93606
🔗 NVD:
https://nvd.nist.gov/vuln/detail/CVE-2026-93606
#CyberSecurity #CVE #vm2 #NodeJS #SandboxEscape #CyberForge