From 97b6424ebe659f68d24040af1eab654b91ea0429 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:30:25 -0700 Subject: [PATCH] fix(security): add cmd validation to Sprite runSprite() and runSpriteSilent() (#2904) Mirrors the guard already in interactiveSession() and all other clouds. Null bytes in cmd could truncate commands at the C level. Fixes #2903 Agent: security-auditor Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- packages/cli/src/__tests__/sprite-cov.test.ts | 14 ++++++++++++++ packages/cli/src/sprite/sprite.ts | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/packages/cli/src/__tests__/sprite-cov.test.ts b/packages/cli/src/__tests__/sprite-cov.test.ts index d5bf5136..2d42395d 100644 --- a/packages/cli/src/__tests__/sprite-cov.test.ts +++ b/packages/cli/src/__tests__/sprite-cov.test.ts @@ -488,6 +488,20 @@ describe("sprite/destroyServer", () => { }); }); +// ─── runSprite validation ──────────────────────────────────────────────────── + +describe("sprite/runSprite validation", () => { + it("rejects empty command", async () => { + const { runSprite } = await import("../sprite/sprite"); + await expect(runSprite("")).rejects.toThrow("Invalid command"); + }); + + it("rejects null byte in command", async () => { + const { runSprite } = await import("../sprite/sprite"); + await expect(runSprite("echo\x00hello")).rejects.toThrow("Invalid command"); + }); +}); + // ─── runSprite ─────────────────────────────────────────────────────────────── describe("sprite/runSprite", () => { diff --git a/packages/cli/src/sprite/sprite.ts b/packages/cli/src/sprite/sprite.ts index b0621846..d9232336 100644 --- a/packages/cli/src/sprite/sprite.ts +++ b/packages/cli/src/sprite/sprite.ts @@ -478,6 +478,9 @@ export function getVmConnection(): VMConnection { * Run a command on the remote sprite. Retries on transient errors. */ export async function runSprite(cmd: string, timeoutSecs?: number): Promise { + if (!cmd || /\0/.test(cmd)) { + throw new Error("Invalid command: must be non-empty and must not contain null bytes"); + } const spriteCmd = getSpriteCmd()!; await spriteRetry("sprite exec", async () => { const proc = Bun.spawn( @@ -515,6 +518,9 @@ export async function runSprite(cmd: string, timeoutSecs?: number): Promise { + if (!cmd || /\0/.test(cmd)) { + throw new Error("Invalid command: must be non-empty and must not contain null bytes"); + } const spriteCmd = getSpriteCmd()!; const proc = Bun.spawn( [