fix(security): base64-encode values in /etc/spawn/secrets to prevent shell injection

Replace shell-sourceable export NAME="VALUE" format with NAME=BASE64VALUE,
decoded at source-time via a loader in ~/.bashrc. This eliminates deferred
code execution when API keys contain quotes, newlines, or shell metacharacters.

Also removes old `source /etc/spawn/secrets` lines from ~/.bashrc in favor
of the safe base64-decoding loader.

Fixes #3361

Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
B 2026-04-25 08:22:00 +00:00
parent cdf2bec2d1
commit 5c4ee735bd

View file

@ -390,11 +390,15 @@ async function applySetupStep(runner: CloudRunner, step: SetupStep): Promise<voi
if (value) {
const escapedName = step.name.replace(/[^A-Za-z0-9_]/g, "");
const b64Val = Buffer.from(value).toString("base64");
// Store as NAME=BASE64VALUE (not shell-executable) to prevent injection
await runner.runServer(
`mkdir -p /etc/spawn && printf 'export %s="%s"\\n' '${escapedName}' "$(echo '${b64Val}' | base64 -d)" >> /etc/spawn/secrets && chmod 600 /etc/spawn/secrets`,
`mkdir -p /etc/spawn && printf '%s=%s\\n' '${escapedName}' '${b64Val}' >> /etc/spawn/secrets && chmod 600 /etc/spawn/secrets`,
);
// Install a loader that decodes base64 at source-time instead of shell-sourcing
const loaderSnippet =
'while IFS="=" read -r k v; do [ -n "$k" ] && export "$k=$(printf "%s" "$v" | base64 -d)"; done < /etc/spawn/secrets';
await runner.runServer(
`grep -q '/etc/spawn/secrets' ~/.bashrc 2>/dev/null || echo 'source /etc/spawn/secrets 2>/dev/null' >> ~/.bashrc`,
`grep -q 'while IFS.*secrets' ~/.bashrc 2>/dev/null || { sed -i '/source.*\\/etc\\/spawn\\/secrets/d' ~/.bashrc 2>/dev/null; echo '${loaderSnippet}' >> ~/.bashrc; }`,
);
logInfo(` ${step.name} saved`);
} else {