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 },