Skip to content

Commit 3b61c22

Browse files
la14-1louisgvclaude
authored
fix(security): validate script templates before base64 encoding (#3132)
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: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9895d6e commit 3b61c22

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/spawn",
3-
"version": "0.30.1",
3+
"version": "0.30.2",
44
"type": "module",
55
"bin": {
66
"spawn": "cli.js"

packages/cli/src/shared/agent-setup.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,27 @@ export interface CloudRunner {
4242
downloadFile(remotePath: string, localPath: string): Promise<void>;
4343
}
4444

45+
// ─── Script template validation ────────────────────────────────────────────
46+
47+
/**
48+
* Validate that a script template string does not contain JS template
49+
* interpolation patterns (`${...}`) before it is base64-encoded for shell
50+
* injection into systemd units or remote commands.
51+
*
52+
* Defense-in-depth: the scripts are currently static string arrays joined
53+
* with `\n`, so they should never contain interpolation markers. This guard
54+
* catches future regressions where a developer might accidentally introduce
55+
* template literal interpolation before encoding.
56+
*
57+
* Note: backticks alone are allowed (used in markdown content for skill
58+
* files), but `${` is always rejected as it indicates JS interpolation.
59+
*/
60+
export function validateScriptTemplate(script: string, label: string): void {
61+
if (/\$\{/.test(script)) {
62+
throw new Error(`Script template "${label}" contains \${} interpolation — refusing to encode`);
63+
}
64+
}
65+
4566
// ─── Install helpers ────────────────────────────────────────────────────────
4667

4768
async function installAgent(
@@ -550,6 +571,9 @@ export async function startGateway(runner: CloudRunner): Promise<void> {
550571
"WantedBy=multi-user.target",
551572
].join("\n");
552573

574+
validateScriptTemplate(wrapperScript, "gateway-wrapper");
575+
validateScriptTemplate(unitFile, "gateway-unit");
576+
553577
const wrapperB64 = Buffer.from(wrapperScript).toString("base64");
554578
const unitB64 = Buffer.from(unitFile).toString("base64");
555579
if (!/^[A-Za-z0-9+/=]+$/.test(wrapperB64)) {
@@ -811,6 +835,10 @@ export async function setupAutoUpdate(runner: CloudRunner, agentName: string, up
811835
"WantedBy=timers.target",
812836
].join("\n");
813837

838+
validateScriptTemplate(wrapperScript, "auto-update-wrapper");
839+
validateScriptTemplate(unitFile, "auto-update-unit");
840+
validateScriptTemplate(timerFile, "auto-update-timer");
841+
814842
const wrapperB64 = Buffer.from(wrapperScript).toString("base64");
815843
const unitB64 = Buffer.from(unitFile).toString("base64");
816844
const timerB64 = Buffer.from(timerFile).toString("base64");

packages/cli/src/shared/spawn-skill.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import type { CloudRunner } from "./agent-setup.js";
66

7-
import { wrapSshCall } from "./agent-setup.js";
7+
import { validateScriptTemplate, wrapSshCall } from "./agent-setup.js";
88
import { asyncTryCatchIf, isOperationalError } from "./result.js";
99
import { logInfo, logWarn } from "./ui.js";
1010

@@ -158,6 +158,8 @@ export async function injectSpawnSkill(runner: CloudRunner, agentName: string):
158158
return;
159159
}
160160

161+
validateScriptTemplate(config.content, `spawn-skill-${agentName}`);
162+
161163
const b64 = Buffer.from(config.content).toString("base64");
162164
if (!/^[A-Za-z0-9+/=]+$/.test(b64)) {
163165
throw new Error("Unexpected characters in base64 output");

0 commit comments

Comments
 (0)