REPRO-2026-00224: Verified Repro With Script Download
REPRO-2026-00224: Node.js task runner improperly escapes single quotes on Unix, enabling shell syntax breakouts and potential command injection via node --run arguments.
REPRO-2026-00224 is verified against node · nodejs. Affected versions: v22.23.0, v24.17.0, v26.3.1 confirmed vulnerable on Linux x64. --run was added in v22.0.0, older versions without --run are not affected. Windows not tested. Fixed in Commit e76c573e4546ce9e89e0dd954f80aaba32148a48. Vulnerability class: Command Injection. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00224.
What Is REPRO-2026-00224?
REPRO-2026-00224 documents a high-severity command-injection issue (CWE-77) in Node.js's task runner. The EscapeShell() routine used by node --run improperly escapes single quotes on POSIX shells, allowing crafted arguments to break out of the quoted context and inject shell syntax. Pruva reproduced it (reproduction REPRO-2026-00224).
REPRO-2026-00224 Severity
REPRO-2026-00224 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected node Versions
node · nodejs versions v22.23.0, v24.17.0, v26.3.1 confirmed vulnerable on Linux x64. --run was added in v22.0.0, older versions without --run are not affected. Windows not tested. are affected.
How to Reproduce REPRO-2026-00224
pruva-verify REPRO-2026-00224 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00224/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for REPRO-2026-00224
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Positional arguments passed to `node --run <task> -- <args>` containing a single quote followed by shell metacharacters (e.g. "x';id > MARKER;echo INJECTION_PROVEN #")
- node --run
- ProcessRunner::ProcessRunner
- EscapeShell (replaces ' with \' and wraps in single quotes)
- uv_spawn /bin/sh -c with the concatenated command string
- shell interprets broken escaping as command syntax
- injected id/echo commands execute
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for REPRO-2026-00224
Node.js' task runner (node --run <task> -- <args>) uses an EscapeShell() routine in src/node_task_runner.cc to safely quote positional arguments before concatenating them into a /bin/sh -c command string. On POSIX systems, the vulnerable code escaped each single quote (') by rewriting it to \' and wrapping the result in single quotes. Because a backslash is literal inside POSIX single-quoted strings, the rewritten ' actually terminates the single-quoted context early, leaving the remainder of the argument to be interpreted as raw shell syntax. An attacker-controlled argument containing a single quote followed by shell metacharacters can break out and inject arbitrary commands. The fix (commit e76c573e4546ce9e89e0dd954f80aaba32148a48) replaces \' with the POSIX-safe sequence '"'"' (close single-quote, double-quote the literal quote, reopen single-quote).
- Package/component affected: Node.js core —
src/node_task_runner.cc, theEscapeShell()function used bynode --run. - Affected versions: All Node.js builds that include the task runner (
--runflag) and are prior to commite76c573e4546ce9e89e0dd954f80aaba32148a48. Verified vulnerable on system Node.js v24.18.0. - Risk level and consequences: High. Any application that forwards user-controlled data as arguments to
node --run(e.g., a CI runner, a build tool, or a web backend that shells out tonode --run) is vulnerable to command injection — arbitrary command execution with the privileges of the Node.js process. Even without metacharacters, a benign single quote in an argument (e.g., a person's name like "I'm") causes a/bin/shsyntax error, breaking the script entirely (denial of service).
Impact Parity
- Disclosed/claimed maximum impact: Command injection / code execution via
node --runarguments on Unix-like systems. - Reproduced impact from this run: Full command injection confirmed. An argument
x';id > MARKER;echo INJECTION_PROVEN #causes theidcommand to execute (writing its output to a marker file) andecho INJECTION_PROVENto print to stdout — arbitrary attacker-controlled commands run via/bin/sh -c. Additionally, the benign argument"I think therefore I'm"causes a/bin/shsyntax error (DoS / broken argument passing). - Parity:
full— the claimed code-execution impact was demonstrated through the realnode --runCLI entrypoint. - Not demonstrated: N/A — code execution was achieved.
Root Cause
In POSIX shells, single-quoted strings preserve every character literally — a backslash has no special meaning inside single quotes. The vulnerable EscapeShell() code:
std::string escaped =
std::regex_replace(std::string(input), std::regex("'"), "\\'");
escaped = "'" + escaped + "'";
rewrites each ' as \' and wraps in '...'. For input x';id > M #:
- After replace:
x\';id > M # - After wrap:
'x\';id > M #'
/bin/sh -c parses 'x\' as a single-quoted string containing x\ (the backslash is literal). The ' after \ closes the single-quote context. The remainder ;id > M # is now unquoted shell syntax: ; is a command separator, id > M executes id with output redirected to M, and # begins a comment that consumes the trailing wrap-quote ' — avoiding any syntax error.
The correct POSIX-safe technique is '"'"' (close the single quote, insert a literal ' via double quotes, reopen the single quote). The fix commit applies this:
std::regex_replace(std::string(input), std::regex("'"), "'\"'\"'");
With the fix, the same input becomes 'x'"'"';id > M #', which /bin/sh parses as a single literal argument x';id > M # with no shell interpretation.
Fix commit: e76c573e4546ce9e89e0dd954f80aaba32148a48 ("src: fix escaping of single quotes in task runner", PR #64089, ref. HackerOne #3817602).
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh - What the script does:
- Reads
bundle/project_cache_context.jsonto locate the prepared Node.js source cache. - Builds (or reuses pre-built) Node.js binaries at the vulnerable commit (
e76c573^) and the fixed commit (e76c573). - Creates a test project with
package.jsoncontaining an npm scriptshowargsmapped toecho. - Runs two attempts per test for both builds:
- Test A (DoS):
node --run showargs -- "I think therefore I'm"— vulnerable build produces/bin/sh: Syntax error: Unterminated quoted string; fixed build printsI think therefore I'mcleanly. - Test B (Command injection):
node --run showargs -- "x';id > MARKER;echo INJECTION_PROVEN #"— vulnerable build creates a marker file containingidoutput and printsINJECTION_PROVEN; fixed build passes the argument literally with no marker file. - Test C (Ticket's exact payload):
node --run showargs -- "foo' ; id ; '"— vulnerable build produces a syntax error (DoS only, since the trailing unbalanced quote is not commented out); fixed build passes the argument safely.
- Test A (DoS):
- Writes
bundle/repro/runtime_manifest.jsonwith runtime evidence.
- Reads
- Expected evidence of reproduction:
- Marker file (
marker_vuln_attempt*.txt) containinguid=...output from the injectedidcommand. - Test logs showing
INJECTION_PROVENin stdout for the vulnerable build. - Test logs showing
/bin/sh: Syntax error: Unterminated quoted stringfor DoS tests. - Absence of marker file and clean literal output for the fixed build (negative control).
- Marker file (
Evidence
- Log file locations:
bundle/logs/test_A_vuln_attempt{1,2}.log— DoS test on vulnerable buildbundle/logs/test_A_fixed_attempt{1,2}.log— DoS test on fixed buildbundle/logs/test_B_vuln_attempt{1,2}.log— Injection test on vulnerable buildbundle/logs/test_B_fixed_attempt{1,2}.log— Injection test on fixed buildbundle/logs/test_C_vuln_attempt{1,2}.log— Ticket payload on vulnerable buildbundle/logs/test_C_fixed_attempt{1,2}.log— Ticket payload on fixed buildbundle/logs/binary_info.txt— Node.js binary versions/pathsbundle/logs/source_diff.txt— Vulnerable vs. fixed EscapeShell sourcebundle/repro/artifacts/marker_vuln_attempt*.txt— Proof of injectedidexecution
- Key excerpts (pre-verified on system Node.js v24.18.0):
- Injection: stdout shows
["x\\"]thenINJECTION_PROVEN; marker file containsuid=1000(vscode) gid=1000(vscode) groups=1000(vscode),969(969) - DoS:
/bin/sh: 1: Syntax error: Unterminated quoted string
- Injection: stdout shows
- Environment: Linux x86_64,
/bin/sh(dash), Node.js built from source at commitse76c573^(vulnerable) ande76c573(fixed). System Node.js v24.18.0 also confirmed vulnerable.
Recommendations / Next Steps
- Suggested fix approach: Apply commit
e76c573e4546ce9e89e0dd954f80aaba32148a48— replace\\'with'\"'\"'in the POSIX branch ofEscapeShell(). This is the canonical POSIX-safe single-quote escaping technique. - Upgrade guidance: Upgrade to any Node.js release that includes commit
e76c573. All versions prior to this commit that ship the--runtask runner are affected. - Testing recommendations: Add regression tests that pass arguments containing single quotes, semicolons, and comment characters through
node --runand verify the shell receives them as literal values. The existingtest/cctest/test_node_task_runner.ccEscapeShellunit test should be expanded to cover the'"'"'output and end-to-end--runinvocations with shell metacharacters.
Additional Notes
- Idempotency: The script cleans up marker files before each injection test and recreates the test project from scratch, so repeated runs produce consistent results.
- Key insight: The ticket's exact payload
foo' ; id ; 'only causes a syntax error (DoS) because the trailing wrap-quote'opens an unterminated single-quoted context, causing/bin/shto reject the entire command before executing anything. The modified payloadx';id > MARKER;echo INJECTION_PROVEN #uses a#comment to consume the trailing wrap-quote, producing a syntactically valid shell command in which the injectedidandechocommands execute. This achieves the full code-execution impact claimed by the ticket. - Why the
#technique works: After the broken\'escaping closes the single-quote context, the#(preceded by whitespace) starts a shell comment that extends to end-of-line, swallowing the wrap's trailing'. The commands before#(id,echo) execute normally. - Platform scope: This vulnerability is POSIX-only. The Windows branch of
EscapeShell()uses a different escaping strategy (double-quote based) and is not affected.
REPRO-2026-00224 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
cd /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/repo && git log --oneline -5 && echo "---FIX COMMIT---" && git log --oneline -1 e76c573e4546ce9e89e0dd954f80aaba32148a48 2>&1 || echo "commit not found locally"ea60060617 vfs: avoid recursive readdir symlink cycles f2f241a40f stream: normalize Broadcast.from() byte inputs 0c59d78639 tools: update nixpkgs-unstable to 89570f24e97e614aa34aa9ab1c927b6578a beb9d2bbfc deps: update nghttp3 to 1.17.0 f96a639edd deps: update googletest to 8b53336594cc52213c6c2c7a0b29194fa896d039 ---FIX COMMIT--- e76c573e45 src: fix escaping of single quotes in task runner
Artifacts and Evidence for REPRO-2026-00224
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix REPRO-2026-00224
Upgrade node · nodejs to Commit e76c573e4546ce9e89e0dd954f80aaba32148a48 or later.
FAQ: REPRO-2026-00224
How does the exploit work?
node --run <script> -- <args> that contains a single quote followed by shell metacharacters breaks out of the single-quoted context once EscapeShell() mis-escapes it, letting the remainder of the argument be interpreted by /bin/sh as shell syntax — causing syntax errors or, with a crafted payload, command injection.Which Node.js versions are affected, and where is it fixed?
How severe is this issue?
node --run is vulnerable to command injection with the privileges of the Node.js process.How can I reproduce this Node.js task runner bug?
node --run <script> -- <crafted-arg> where the argument contains a single quote followed by shell metacharacters, and confirm the shell syntax breakout; compare against a build containing the fix commit, which no longer breaks out.References for REPRO-2026-00224
Authoritative sources for REPRO-2026-00224 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.