← Back to Blog
Security11 min read

Node2Shell: RCE Chains in Node.js

October 23, 2026

Node.js runtimes power the serving layer for a majority of modern web applications and a growing share of API backends. The attack surface is distinct from PHP — Node.js is a single-threaded event loop with explicit async semantics, a module system that privileges dependencies over configuration, and a historical reliance on JavaScript's prototype chain. Those three traits produce the Node2Shell chain classes dominating 2026 incident forensics: prototype pollution, insecure deserialisation, template injection, child_process abuse, and malicious npm packages. This document walks each chain and the defences that actually hold.

Chain 1: Prototype Pollution to RCE

Prototype pollution writes a property onto Object.prototype that then appears on every object the application touches. By itself it is a denial-of-service and logic-corruption bug. In combination with a gadget — a sink that reads a property from an object and uses it in a dangerous way — it is remote code execution.

The classical gadget is child_process.spawn reading a shell option defaulting to false. Pollute Object.prototype.shell = "/bin/sh" and every subsequent spawn runs through a shell, converting a child_process call with an attacker-controlled argument into an injection. Template engines that read loader options from arbitrary objects (Pug, Handlebars with helpers, EJS legacy variants) have gadgets where a polluted property reroutes template loading to attacker-controlled filesystem paths or inline source strings.

Entry points: Express req.query with a parser like qs that accepts bracket notation, JSON body parsers that deep-merge into configuration objects, YAML loaders without safe mode, and the lodash / merge-deep / set-value family of utility functions when invoked on attacker-controlled paths. Mitigations: Node.js 22+ --disable-proto=delete, Object.create(null) for maps that will receive untrusted data, schema validation on every input boundary, and never calling lodash.merge on untrusted input (switch to lodash.mergeWith with a prototype-aware customiser).

Chain 2: Insecure Deserialisation

Node.js has no built-in serialisation format equivalent to Java's ObjectInputStream, but the ecosystem replicates the risk through node-serialize,serialize-javascript,cryo, and homegrown IPC formats that reconstruct function objects from strings. The CVE catalogue for node-serialize is a decade of RCE bugs with the same signature: an IIFE embedded in the serialised payload triggers on deserialisation.

Any application that persists session state, caches arbitrary objects, or communicates between workers using these libraries with attacker-reachable payloads is at risk. The defence is to switch to a schema-first format (Protocol Buffers, Avro) or a validated JSON schema, and to treat any legacy eval-based deserialisation as code execution surface rather than data parsing.

Chain 3: Template Injection

Handlebars, Pug, EJS, and Mustache are the dominant Node template engines. Each has had injection bugs — Handlebars prototype access, Pug reserved-identifier abuse, EJS outputFunctionName injection with CVE-2022-29078. The exploitation pattern mirrors Freemarker and Smarty: a user-controlled string reaches the template compiler as source, the compiler emits JavaScript that runs in the server's process, RCE.

The fix is the same across engines — pass user data as template variables rendered inside the template, never as part of the compiled template source. Mail systems and content management flows that let administrators "customise templates" are the dominant attack surface because administrator-controlled templates and attacker-controlled administrator sessions converge on the same primitive.

Chain 4: child_process and spawn Injection

The child_process module's exec andspawn are the Node equivalents of PHP's system(). Any invocation that interpolates user input into a shell string is injection.

Common risky patterns: ImageMagick / FFmpeg / pdftotext invocations where filename or parameter values come from user input, git / docker CLI wrappers that take branch or image names from the request, system-ops tooling that lets users specify command-line flags. Fix: use execFile orspawn with an argv array and shell: false — never exec with a formatted string.

Chain 5: Malicious npm Packages

The npm supply-chain attack surface is the single largest Node2Shell vector by package-count. Typosquatting, dependency confusion, maintainer-account takeover, and post-install scripts in otherwise-legitimate transitive dependencies have each shipped malware to production in the past five years. The attacker does not need to exploit the application — they ship code the application will execute by virtue of being installed.

Defences: lockfile integrity (package-lock.json, yarn.lock, pnpm-lock.yaml) with strict checking in CI, provenance verification for critical dependencies, Socket / Snyk / npm audit as pre-merge gates, --ignore-scripts for untrusted installs, pinning transitive dependency versions, and vendor-isolation of internal packages via scoped private registries.

Chain 6: VM Sandbox Escapes

Applications that execute user-supplied JavaScript — code playgrounds, bot platforms, low-code integration builders, LLM-driven code-interpreter features — usually run user code in Node's vm module or a sandbox library like vm2 (now unmaintained). Both have escape histories. The vm module is explicitly not a security boundary in the official docs. vm2 escaped repeatedly in 2023 and the author eventually deprecated the project.

The honest answer for user-supplied JavaScript execution is: run it in a separate process with OS-level sandboxing (seccomp, gVisor, Firecracker) or in a WebAssembly runtime that provides capability-based isolation. The in-process "sandbox" approach never held.

Framework-Specific Patterns

Express: query-parser pollution via qs, rawreq.body deep-merge into config, insecure res.render with user data in the template path.

Next.js: API-route SSRF through fetch with user-supplied URLs, image-optimisation path abuse, legacy middleware that bypasses the edge runtime's stricter sandbox.

NestJS: pipe-validation bypass through inherited DTO properties, class-transformer prototype pollution via plainToInstance on untrusted payloads.

Fastify: schema-validation gaps where additionalProperties defaults are not tightened, body-limit misconfiguration enabling memory-exhaustion attacks.

Detection

Node2Shell detection has three useful layers. Runtime instrumentation (OpenTelemetry with security-relevant spans) catches anomalous child_process invocations and spawn-with-shell patterns. Dependency-change monitoring (Socket, Snyk, GitHub Dependabot with security-advisory-only gating) flags package-version movements that introduce known vulnerable transitive deps. Egress firewall policies on the Node pod / container segment block the first-stage curl / wget that most post-exploitation drops.

Our continuous Node.js pen testing covers all six chain classes above on every release, with payloads calibrated to the specific framework (Express, Next.js, NestJS, Fastify, Koa) and dependencies declared in the application's package.json.

Stop finding vulnerabilities manually

TigerStrike uses AI agents to continuously discover, validate, and exploit vulnerabilities across your applications — so your team can focus on fixing what matters.