fix(security): add cmd validation to Sprite runSprite() and runSpriteSilent() (#2904)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-23 03:30:25 -07:00 committed by GitHub
parent 5392ff2d7a
commit 97b6424ebe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 20 additions and 0 deletions

View file

@ -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 ─────────────────────────────────────────────────────────────── // ─── runSprite ───────────────────────────────────────────────────────────────
describe("sprite/runSprite", () => { describe("sprite/runSprite", () => {

View file

@ -478,6 +478,9 @@ export function getVmConnection(): VMConnection {
* Run a command on the remote sprite. Retries on transient errors. * Run a command on the remote sprite. Retries on transient errors.
*/ */
export async function runSprite(cmd: string, timeoutSecs?: number): Promise<void> { export async function runSprite(cmd: string, timeoutSecs?: number): Promise<void> {
if (!cmd || /\0/.test(cmd)) {
throw new Error("Invalid command: must be non-empty and must not contain null bytes");
}
const spriteCmd = getSpriteCmd()!; const spriteCmd = getSpriteCmd()!;
await spriteRetry("sprite exec", async () => { await spriteRetry("sprite exec", async () => {
const proc = Bun.spawn( const proc = Bun.spawn(
@ -515,6 +518,9 @@ export async function runSprite(cmd: string, timeoutSecs?: number): Promise<void
/** Run a command silently (no stdout/stderr). Throws on failure. */ /** Run a command silently (no stdout/stderr). Throws on failure. */
async function runSpriteSilent(cmd: string): Promise<void> { async function runSpriteSilent(cmd: string): Promise<void> {
if (!cmd || /\0/.test(cmd)) {
throw new Error("Invalid command: must be non-empty and must not contain null bytes");
}
const spriteCmd = getSpriteCmd()!; const spriteCmd = getSpriteCmd()!;
const proc = Bun.spawn( const proc = Bun.spawn(
[ [