From 4f4b535c8d118ba1ee9140f95d61e6e06aaca2fa Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:04:19 -0700 Subject: [PATCH] fix(security): validate remotePath and harden base64 interpolation in uploadConfigFile (#2669) Add strict character validation for remotePath to prevent command injection via crafted paths. Use shellQuote for tempRemote in the shell command. Add a base64 output assertion to document and enforce the safety of single-quoted interpolation for settingsB64. Fixes #2668 Agent: security-auditor Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- packages/cli/package.json | 2 +- packages/cli/src/shared/agent-setup.ts | 27 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 97ec800a..e1e8d1b2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.19.1", + "version": "0.19.2", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 65ada876..ee7716c8 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -59,10 +59,30 @@ async function installAgent( logInfo(`${agentName} installation completed`); } +/** + * Validate that a remote path contains only safe characters. + * Allows shell variable references ($HOME, ${HOME}) but rejects anything + * that could break out of double-quoted shell interpolation. + */ +function validateRemotePath(remotePath: string): void { + // Allow alphanumerics, forward slashes, dots, underscores, tildes, hyphens, + // and shell variable syntax ($, {, }). Reject everything else — especially + // backticks, semicolons, pipes, quotes, newlines, and null bytes. + if (!/^[\w/.~${}:-]+$/.test(remotePath)) { + throw new Error(`uploadConfigFile: remotePath contains unsafe characters: ${remotePath}`); + } + // Block path traversal + if (remotePath.includes("..")) { + throw new Error(`uploadConfigFile: remotePath must not contain "..": ${remotePath}`); + } +} + /** * Upload a config file to the remote machine via a temp file and mv. */ async function uploadConfigFile(runner: CloudRunner, content: string, remotePath: string): Promise { + validateRemotePath(remotePath); + const tmpFile = join(getTmpDir(), `spawn_config_${Date.now()}_${Math.random().toString(36).slice(2)}`); writeFileSync(tmpFile, content, { mode: 0o600, @@ -77,7 +97,7 @@ async function uploadConfigFile(runner: CloudRunner, content: string, remotePath const tempRemote = `/tmp/spawn_config_${Date.now()}`; await runner.uploadFile(tmpFile, tempRemote); await runner.runServer( - `mkdir -p $(dirname "${remotePath}") && chmod 600 '${tempRemote}' && mv '${tempRemote}' "${remotePath}"`, + `mkdir -p $(dirname "${remotePath}") && chmod 600 ${shellQuote(tempRemote)} && mv ${shellQuote(tempRemote)} "${remotePath}"`, ); })(), ), @@ -143,7 +163,12 @@ async function setupClaudeCodeConfig(runner: CloudRunner, apiKey: string): Promi } }`; + // Safety: base64 output only contains [A-Za-z0-9+/=] — never single quotes — + // so interpolating into a single-quoted shell string is safe. const settingsB64 = Buffer.from(settingsJson).toString("base64"); + if (!/^[A-Za-z0-9+/=]+$/.test(settingsB64)) { + throw new Error("Unexpected characters in base64 output"); + } // Build ~/.claude.json on the remote using $HOME so the workspace trust // entry uses the actual home directory path (e.g. /root, /home/user).