From 6dd88a1d19b00a55da7d9c8604bd760b79744b80 Mon Sep 17 00:00:00 2001 From: Matthew-Selvam Date: Sat, 18 Jul 2026 10:25:08 +0530 Subject: [PATCH] fix: escape package names before shell interpolation in npm install (CWE-78) Wraps the already-validated package name with escapeShellArg() before interpolating into 'npm install -g' at all 3 call sites (install_npm_package, install_mcp_server, installNpmPackage), as defense-in-depth alongside the existing character-class validation. Fixes #181 --- src/__tests__/tools-security.test.ts | 2 +- src/agent/tools.ts | 10 ++++++++-- src/self-mod/tools-manager.ts | 7 ++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/__tests__/tools-security.test.ts b/src/__tests__/tools-security.test.ts index 6a9eecd7..a0d70c88 100644 --- a/src/__tests__/tools-security.test.ts +++ b/src/__tests__/tools-security.test.ts @@ -654,7 +654,7 @@ describe("package install inline validation", () => { const tool = tools.find((t) => t.name === "install_npm_package")!; await tool.execute({ package: "axios" }, ctx); expect(conway.execCalls.length).toBe(1); - expect(conway.execCalls[0].command).toBe("npm install -g axios"); + expect(conway.execCalls[0].command).toBe("npm install -g 'axios'"); }); it("install_npm_package allows scoped packages", async () => { diff --git a/src/agent/tools.ts b/src/agent/tools.ts index 0d2db864..d38fee1c 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -568,7 +568,10 @@ export function createBuiltinTools(sandboxId: string): AutomatonTool[] { if (!/^[@a-zA-Z0-9._\/-]+$/.test(pkg)) { return `Blocked: invalid package name "${pkg}"`; } - const result = await ctx.conway.exec(`npm install -g ${pkg}`, 60000); + const result = await ctx.conway.exec( + `npm install -g ${escapeShellArg(pkg)}`, + 60000, + ); const { ulid } = await import("ulid"); ctx.db.insertModification({ @@ -964,7 +967,10 @@ Model: ${ctx.inference.getDefaultModel()} if (!/^[@a-zA-Z0-9._\/-]+$/.test(pkg)) { return `Blocked: invalid package name "${pkg}"`; } - const result = await ctx.conway.exec(`npm install -g ${pkg}`, 60000); + const result = await ctx.conway.exec( + `npm install -g ${escapeShellArg(pkg)}`, + 60000, + ); if (result.exitCode !== 0) { return `Failed to install MCP server: ${result.stderr}`; diff --git a/src/self-mod/tools-manager.ts b/src/self-mod/tools-manager.ts index 6fa7ed84..9a5a48f2 100644 --- a/src/self-mod/tools-manager.ts +++ b/src/self-mod/tools-manager.ts @@ -12,6 +12,11 @@ import type { import { logModification } from "./audit-log.js"; import { ulid } from "ulid"; +/** Escape a string for safe shell interpolation. */ +function escapeShellArg(arg: string): string { + return `'${arg.replace(/'/g, "'\\''")}'`; +} + /** * Install an npm package globally in the sandbox. */ @@ -29,7 +34,7 @@ export async function installNpmPackage( } const result = await conway.exec( - `npm install -g ${packageName}`, + `npm install -g ${escapeShellArg(packageName)}`, 120000, );