From 4d7237fa31e294d18af6a4101176667ed98dadef Mon Sep 17 00:00:00 2001 From: "Dexter.k" <164054284+rootvector2@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:30:54 +0000 Subject: [PATCH] fix: escape executable name in libnpmexec run-script (#9436) run() in libnpmexec wraps the executable name in double quotes before it becomes the npx script string, but double quotes still expand $(), backticks, $var and a closing quote, so a package whose bin key holds shell metacharacters breaks out once the name reaches sh -c. The bin name comes straight from a published package.json. Switch the non-Windows branch to single-quote escaping so the name is taken literally. (cherry picked from commit 6901bb185a5ca323d6d76561136a906a5023ea6d) --- workspaces/libnpmexec/lib/run-script.js | 4 +++- workspaces/libnpmexec/test/run-script.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/workspaces/libnpmexec/lib/run-script.js b/workspaces/libnpmexec/lib/run-script.js index 13f16a74eb8a0..2de40454f9dcf 100644 --- a/workspaces/libnpmexec/lib/run-script.js +++ b/workspaces/libnpmexec/lib/run-script.js @@ -19,7 +19,9 @@ const run = async ({ // necessary for preventing bash/cmd keywords from overriding if (!isWindowsShell) { if (args.length > 0) { - args[0] = '"' + args[0] + '"' + // single-quote so shell metacharacters in the executable name are taken + // literally; double quotes still expand $(), backticks, $var and " + args[0] = `'${args[0].replace(/'/g, `'\\''`)}'` } } diff --git a/workspaces/libnpmexec/test/run-script.js b/workspaces/libnpmexec/test/run-script.js index 61937098b7e83..5c92ce19397f6 100644 --- a/workspaces/libnpmexec/test/run-script.js +++ b/workspaces/libnpmexec/test/run-script.js @@ -130,6 +130,20 @@ t.test('isWindows', async t => { await runScript() }) +t.test('escapes executable name to neutralize shell metacharacters', async t => { + let pkg + const { runScript } = await mockRunScript(t, { + 'ci-info': { isCI: true }, + '@npmcli/run-script': async (opts) => { + pkg = opts.pkg + }, + '../lib/is-windows.js': false, + }) + + await runScript({ args: [`evil'; touch pwned #`] }) + t.equal(pkg.scripts.npx, `'evil'\\''; touch pwned #'`) +}) + t.test('isNotWindows', async t => { const { runScript } = await mockRunScript(t, { 'ci-info': { isCI: true },