From 5cb7cd610027607d5dd5146371b5d12287340b3e Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 18:48:37 +0530 Subject: [PATCH 1/6] refactor: prevent command injection --- lib/shared/get-package-manager.ts | 146 +++++++++++++++-------- tests/shared/get-package-manager.test.ts | 12 ++ 2 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 tests/shared/get-package-manager.test.ts diff --git a/lib/shared/get-package-manager.ts b/lib/shared/get-package-manager.ts index da9e84184..4892af964 100644 --- a/lib/shared/get-package-manager.ts +++ b/lib/shared/get-package-manager.ts @@ -1,6 +1,6 @@ import fs from "fs" import kleur from "kleur" -import { execSync } from "node:child_process" +import { spawnSync } from "node:child_process" function detectPackageManager(): "npm" | "yarn" | "pnpm" | "bun" { const userAgent = process.env.npm_config_user_agent || "" @@ -45,82 +45,132 @@ export interface PackageManager { getInstallAllCommand: () => string } +function validatePackageNames(names: string[]) { + for (const n of names) { + const specIsSafe = /^[a-z0-9@.\-_/:]+$/i.test(n) && !n.includes("..") + if (!specIsSafe) { + throw new Error(`Refusing to process invalid package spec: ${n}`) + } + } +} + export function getPackageManager(): PackageManager { const pm = detectPackageManager() return { name: pm, uninstall: ({ name, cwd }) => { - let uninstallCommand: string - if (pm === "yarn") { - uninstallCommand = `yarn remove ${name}` - } else if (pm === "pnpm") { - uninstallCommand = `pnpm remove ${name}` - } else if (pm === "bun") { - uninstallCommand = `bun remove ${name}` - } else { - uninstallCommand = `npm uninstall ${name}` + const names = name.split(/\s+/).filter(Boolean) + if (names.length === 0) return + validatePackageNames(names) + + let args: string[] + if (pm === "yarn") args = ["remove", ...names] + else if (pm === "pnpm") args = ["remove", ...names] + else if (pm === "bun") args = ["remove", ...names] + else args = ["uninstall", ...names] + + const output = spawnSync(pm, args, { stdio: "pipe", cwd }) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) } - execSync(uninstallCommand, { stdio: "pipe", cwd }) }, install: ({ name, cwd }) => { - let installCommand: string - if (pm === "yarn") { - installCommand = `yarn add ${name}` - } else if (pm === "pnpm") { - installCommand = `pnpm add ${name}` - } else if (pm === "bun") { - installCommand = `bun add ${name}` - } else { - installCommand = `npm install ${name}` - } - console.log(kleur.gray(`> ${installCommand}`)) - const output = execSync(installCommand, { + const names = name.split(/\s+/).filter(Boolean) + if (names.length === 0) return + validatePackageNames(names) + + let args: string[] + if (pm === "yarn") args = ["add", ...names] + else if (pm === "pnpm") args = ["add", ...names] + else if (pm === "bun") args = ["add", ...names] + else args = ["install", ...names] + + console.log(kleur.gray(`> ${pm} ${args.join(" ")}`)) + const output = spawnSync(pm, args, { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output) { - process.stdout.write(output) + if (output.stdout) process.stdout.write(output.stdout) + if (output.stderr) process.stderr.write(output.stderr) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) } }, update: ({ name, cwd }) => { - let updateCommand: string - if (pm === "yarn") { - updateCommand = `yarn upgrade ${name}` - } else if (pm === "pnpm") { - updateCommand = `pnpm update ${name}` - } else if (pm === "bun") { - updateCommand = `bun update ${name}` - } else { - updateCommand = `npm update ${name}` - } - console.log(kleur.gray(`> ${updateCommand}`)) - const output = execSync(updateCommand, { + const names = name.split(/\s+/).filter(Boolean) + if (names.length === 0) return + validatePackageNames(names) + + let args: string[] + if (pm === "yarn") args = ["upgrade", ...names] + else if (pm === "pnpm") args = ["update", ...names] + else if (pm === "bun") args = ["update", ...names] + else args = ["update", ...names] + + console.log(kleur.gray(`> ${pm} ${args.join(" ")}`)) + const output = spawnSync(pm, args, { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output) { - process.stdout.write(output) + if (output.stdout) process.stdout.write(output.stdout) + if (output.stderr) process.stderr.write(output.stderr) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) } }, init: ({ cwd }) => { - const initCommand = getInitCommand() - execSync(initCommand, { stdio: "inherit", cwd }) + let args: string[] + if (pm === "yarn") args = ["init", "-y"] + else if (pm === "pnpm") args = ["init"] + else if (pm === "bun") args = ["init", "-y"] + else args = ["init", "-y"] + + const output = spawnSync(pm, args, { stdio: "inherit", cwd }) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) + } }, installDeps: ({ deps, cwd, dev }) => { - const installCommand = getInstallDepsCommand(deps, dev) - execSync(installCommand, { stdio: "inherit", cwd }) + if (deps.length === 0) return + validatePackageNames(deps) + + let args: string[] + if (pm === "bun") args = ["add", dev ? "-d" : "", ...deps].filter(Boolean) + else if (pm === "yarn") + args = ["add", dev ? "-D" : "", ...deps].filter(Boolean) + else if (pm === "pnpm") + args = ["add", dev ? "-D" : "", ...deps].filter(Boolean) + else args = ["install", dev ? "-D" : "", ...deps].filter(Boolean) + + const output = spawnSync(pm, args, { stdio: "inherit", cwd }) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) + } }, getInitCommand, getInstallDepsCommand, installAll: ({ cwd }) => { - const installCommand = getInstallAllCommand() - console.log(kleur.gray(`> ${installCommand}`)) - const output = execSync(installCommand, { + let args: string[] + if (pm === "yarn") args = ["install"] + else if (pm === "pnpm") args = ["install"] + else if (pm === "bun") args = ["install"] + else args = ["install"] + + console.log(kleur.gray(`> ${pm} install`)) + const output = spawnSync(pm, args, { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output) { - process.stdout.write(output) + if (output.stdout) process.stdout.write(output.stdout) + if (output.stderr) process.stderr.write(output.stderr) + if (output.error) throw output.error + if (output.status !== 0) { + throw new Error(`Command failed with exit code ${output.status}`) } }, getInstallAllCommand, diff --git a/tests/shared/get-package-manager.test.ts b/tests/shared/get-package-manager.test.ts new file mode 100644 index 000000000..9eee798bd --- /dev/null +++ b/tests/shared/get-package-manager.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test" +import { getPackageManager } from "../../lib/shared/get-package-manager" + +test("get-package-manager command injection prevention", () => { + const pm = getPackageManager() + + expect(() => pm.install({ name: "x; ls", cwd: "." })).toThrow() + expect(() => pm.install({ name: "pkg|curl", cwd: "." })).toThrow() + expect(() => pm.install({ name: "pkg>file", cwd: "." })).toThrow() + expect(() => pm.install({ name: "../pkg", cwd: "." })).toThrow() + expect(() => pm.install({ name: "$(whoami)", cwd: "." })).toThrow() +}) From be4aaa186bcabfee07946d5e5291c2807051d143 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 19:48:20 +0530 Subject: [PATCH 2/6] refactor: abstract spawn process error handling into helper and update gitignore with new patterns --- lib/shared/get-package-manager.ts | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/shared/get-package-manager.ts b/lib/shared/get-package-manager.ts index 4892af964..f87a769a3 100644 --- a/lib/shared/get-package-manager.ts +++ b/lib/shared/get-package-manager.ts @@ -54,6 +54,26 @@ function validatePackageNames(names: string[]) { } } +function handleSpawnOutput(output: ReturnType) { + if (output.error) throw output.error + if (output.status !== 0) { + const errMessage = [ + `Command failed with exit code ${output.status}`, + output.stdout ? output.stdout.toString() : "", + output.stderr ? output.stderr.toString() : "", + ] + .filter(Boolean) + .join("\n") + const err = new Error(errMessage) + Object.assign(err, { + status: output.status, + stdout: output.stdout, + stderr: output.stderr, + }) + throw err + } +} + export function getPackageManager(): PackageManager { const pm = detectPackageManager() return { @@ -70,10 +90,7 @@ export function getPackageManager(): PackageManager { else args = ["uninstall", ...names] const output = spawnSync(pm, args, { stdio: "pipe", cwd }) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, install: ({ name, cwd }) => { const names = name.split(/\s+/).filter(Boolean) @@ -93,10 +110,7 @@ export function getPackageManager(): PackageManager { }) if (output.stdout) process.stdout.write(output.stdout) if (output.stderr) process.stderr.write(output.stderr) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, update: ({ name, cwd }) => { const names = name.split(/\s+/).filter(Boolean) @@ -116,10 +130,7 @@ export function getPackageManager(): PackageManager { }) if (output.stdout) process.stdout.write(output.stdout) if (output.stderr) process.stderr.write(output.stderr) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, init: ({ cwd }) => { let args: string[] @@ -129,10 +140,7 @@ export function getPackageManager(): PackageManager { else args = ["init", "-y"] const output = spawnSync(pm, args, { stdio: "inherit", cwd }) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, installDeps: ({ deps, cwd, dev }) => { if (deps.length === 0) return @@ -147,10 +155,7 @@ export function getPackageManager(): PackageManager { else args = ["install", dev ? "-D" : "", ...deps].filter(Boolean) const output = spawnSync(pm, args, { stdio: "inherit", cwd }) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, getInitCommand, getInstallDepsCommand, @@ -168,10 +173,7 @@ export function getPackageManager(): PackageManager { }) if (output.stdout) process.stdout.write(output.stdout) if (output.stderr) process.stderr.write(output.stderr) - if (output.error) throw output.error - if (output.status !== 0) { - throw new Error(`Command failed with exit code ${output.status}`) - } + handleSpawnOutput(output) }, getInstallAllCommand, } From cfb662660e1fd38cd072ca51e61a68664b9932f0 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 19:48:22 +0530 Subject: [PATCH 3/6] f --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4ecdbb0da..87a4627e6 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,6 @@ package-lock.json .yalc .yalc.lock .tscircuit +.agents +skills-lock.json +/*.md \ No newline at end of file From 7a51432fb00f10793e2f19a14d41c622a9615099 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 19:49:29 +0530 Subject: [PATCH 4/6] g --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 87a4627e6..4ecdbb0da 100644 --- a/.gitignore +++ b/.gitignore @@ -182,6 +182,3 @@ package-lock.json .yalc .yalc.lock .tscircuit -.agents -skills-lock.json -/*.md \ No newline at end of file From a4746d179645a8781e27a7e5399d4ef5aa958669 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 19:50:27 +0530 Subject: [PATCH 5/6] fssdf --- lib/shared/get-package-manager.ts | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/shared/get-package-manager.ts b/lib/shared/get-package-manager.ts index f87a769a3..e83ad65e0 100644 --- a/lib/shared/get-package-manager.ts +++ b/lib/shared/get-package-manager.ts @@ -2,6 +2,23 @@ import fs from "fs" import kleur from "kleur" import { spawnSync } from "node:child_process" +export interface PackageManager { + name: "npm" | "yarn" | "pnpm" | "bun" + uninstall: (opts: { name: string; cwd: string }) => void + install: (opts: { name: string; cwd: string }) => void + update: (opts: { name: string; cwd: string }) => void + init: (opts: { cwd: string }) => void + installDeps: (opts: { + deps: string[] + cwd: string + dev?: boolean + }) => void + getInitCommand: () => string + getInstallDepsCommand: (deps: string[], dev?: boolean) => string + installAll: (opts: { cwd: string }) => void + getInstallAllCommand: () => string +} + function detectPackageManager(): "npm" | "yarn" | "pnpm" | "bun" { const userAgent = process.env.npm_config_user_agent || "" if (userAgent.startsWith("yarn")) return "yarn" @@ -28,23 +45,6 @@ function detectPackageManager(): "npm" | "yarn" | "pnpm" | "bun" { return "npm" // Default to npm } -export interface PackageManager { - name: "npm" | "yarn" | "pnpm" | "bun" - uninstall: (opts: { name: string; cwd: string }) => void - install: (opts: { name: string; cwd: string }) => void - update: (opts: { name: string; cwd: string }) => void - init: (opts: { cwd: string }) => void - installDeps: (opts: { - deps: string[] - cwd: string - dev?: boolean - }) => void - getInitCommand: () => string - getInstallDepsCommand: (deps: string[], dev?: boolean) => string - installAll: (opts: { cwd: string }) => void - getInstallAllCommand: () => string -} - function validatePackageNames(names: string[]) { for (const n of names) { const specIsSafe = /^[a-z0-9@.\-_/:]+$/i.test(n) && !n.includes("..") From ea0a6981b4c2813ea26592753a828ebb28267fb0 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Mon, 10 Aug 2026 19:53:51 +0530 Subject: [PATCH 6/6] dry --- lib/shared/get-package-manager.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/shared/get-package-manager.ts b/lib/shared/get-package-manager.ts index e83ad65e0..8b8462d93 100644 --- a/lib/shared/get-package-manager.ts +++ b/lib/shared/get-package-manager.ts @@ -55,16 +55,11 @@ function validatePackageNames(names: string[]) { } function handleSpawnOutput(output: ReturnType) { + if (output.stdout) process.stdout.write(output.stdout) + if (output.stderr) process.stderr.write(output.stderr) if (output.error) throw output.error if (output.status !== 0) { - const errMessage = [ - `Command failed with exit code ${output.status}`, - output.stdout ? output.stdout.toString() : "", - output.stderr ? output.stderr.toString() : "", - ] - .filter(Boolean) - .join("\n") - const err = new Error(errMessage) + const err = new Error(`Command failed with exit code ${output.status}`) Object.assign(err, { status: output.status, stdout: output.stdout, @@ -108,8 +103,6 @@ export function getPackageManager(): PackageManager { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output.stdout) process.stdout.write(output.stdout) - if (output.stderr) process.stderr.write(output.stderr) handleSpawnOutput(output) }, update: ({ name, cwd }) => { @@ -128,8 +121,6 @@ export function getPackageManager(): PackageManager { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output.stdout) process.stdout.write(output.stdout) - if (output.stderr) process.stderr.write(output.stderr) handleSpawnOutput(output) }, init: ({ cwd }) => { @@ -171,8 +162,6 @@ export function getPackageManager(): PackageManager { stdio: ["inherit", "pipe", "pipe"], cwd, }) - if (output.stdout) process.stdout.write(output.stdout) - if (output.stderr) process.stderr.write(output.stderr) handleSpawnOutput(output) }, getInstallAllCommand,