From bfbf3a614fc6c41430047c8898631ed64b227f72 Mon Sep 17 00:00:00 2001 From: B <6723574+louisgv@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:01:46 +0000 Subject: [PATCH] fix(security): validate script templates before base64 encoding Add pre-encoding validation to reject ${} interpolation patterns in script template strings before they are base64-encoded and injected into systemd services running with root privileges on remote VMs. Defense-in-depth against future regressions where template variable interpolation before encoding could allow command injection. Fixes #3130 Agent: security-auditor Co-Authored-By: Claude Sonnet 4.5 --- packages/cli/package.json | 2 +- packages/cli/src/shared/agent-setup.ts | 28 ++++++++++++++++++++++++++ packages/cli/src/shared/spawn-skill.ts | 4 +++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8545797e1..27f92ca85 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.30.1", + "version": "0.30.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 823721a76..ff13d3c82 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -42,6 +42,27 @@ export interface CloudRunner { downloadFile(remotePath: string, localPath: string): Promise; } +// ─── Script template validation ──────────────────────────────────────────── + +/** + * Validate that a script template string does not contain JS template + * interpolation patterns (`${...}`) before it is base64-encoded for shell + * injection into systemd units or remote commands. + * + * Defense-in-depth: the scripts are currently static string arrays joined + * with `\n`, so they should never contain interpolation markers. This guard + * catches future regressions where a developer might accidentally introduce + * template literal interpolation before encoding. + * + * Note: backticks alone are allowed (used in markdown content for skill + * files), but `${` is always rejected as it indicates JS interpolation. + */ +export function validateScriptTemplate(script: string, label: string): void { + if (/\$\{/.test(script)) { + throw new Error(`Script template "${label}" contains \${} interpolation — refusing to encode`); + } +} + // ─── Install helpers ──────────────────────────────────────────────────────── async function installAgent( @@ -550,6 +571,9 @@ export async function startGateway(runner: CloudRunner): Promise { "WantedBy=multi-user.target", ].join("\n"); + validateScriptTemplate(wrapperScript, "gateway-wrapper"); + validateScriptTemplate(unitFile, "gateway-unit"); + const wrapperB64 = Buffer.from(wrapperScript).toString("base64"); const unitB64 = Buffer.from(unitFile).toString("base64"); if (!/^[A-Za-z0-9+/=]+$/.test(wrapperB64)) { @@ -811,6 +835,10 @@ export async function setupAutoUpdate(runner: CloudRunner, agentName: string, up "WantedBy=timers.target", ].join("\n"); + validateScriptTemplate(wrapperScript, "auto-update-wrapper"); + validateScriptTemplate(unitFile, "auto-update-unit"); + validateScriptTemplate(timerFile, "auto-update-timer"); + const wrapperB64 = Buffer.from(wrapperScript).toString("base64"); const unitB64 = Buffer.from(unitFile).toString("base64"); const timerB64 = Buffer.from(timerFile).toString("base64"); diff --git a/packages/cli/src/shared/spawn-skill.ts b/packages/cli/src/shared/spawn-skill.ts index 94a37449a..f72e0ee06 100644 --- a/packages/cli/src/shared/spawn-skill.ts +++ b/packages/cli/src/shared/spawn-skill.ts @@ -4,7 +4,7 @@ import type { CloudRunner } from "./agent-setup.js"; -import { wrapSshCall } from "./agent-setup.js"; +import { validateScriptTemplate, wrapSshCall } from "./agent-setup.js"; import { asyncTryCatchIf, isOperationalError } from "./result.js"; import { logInfo, logWarn } from "./ui.js"; @@ -158,6 +158,8 @@ export async function injectSpawnSkill(runner: CloudRunner, agentName: string): return; } + validateScriptTemplate(config.content, `spawn-skill-${agentName}`); + const b64 = Buffer.from(config.content).toString("base64"); if (!/^[A-Za-z0-9+/=]+$/.test(b64)) { throw new Error("Unexpected characters in base64 output");