Fix shell injection — use tee with stdin instead of echo interpolation

This commit is contained in:
2026-04-08 01:36:01 +00:00
parent d9b695d5a0
commit 74e79f2870

View File

@@ -10,6 +10,13 @@ import {
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { CONFIG } from "./config.js";
const SSH_OPTS = [
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=5",
"-i", CONFIG.sshKeyPath,
];
import {
allocateIp,
releaseIp,
@@ -103,7 +110,7 @@ function injectAgentConfig(
{ stdio: "pipe" }
);
// Write config
// Write config (via stdin to avoid shell injection)
const configJson = JSON.stringify({
nick: config.nick,
model: config.model,
@@ -112,26 +119,18 @@ function injectAgentConfig(
port: 6667,
ollama_url: "http://172.16.0.1:11434",
});
execFileSync(
"sudo",
[
"bash",
"-c",
`echo '${configJson}' > ${join(mountPoint, "etc/agent/config.json")}`,
],
{ stdio: "pipe" }
);
const configPath = join(mountPoint, "etc/agent/config.json");
execFileSync("sudo", ["tee", configPath], {
input: configJson,
stdio: ["pipe", "pipe", "pipe"],
});
// Write persona
execFileSync(
"sudo",
[
"bash",
"-c",
`cat > ${join(mountPoint, "etc/agent/persona.md")} << 'PERSONA_EOF'\n${persona}\nPERSONA_EOF`,
],
{ stdio: "pipe" }
);
// Write persona (via stdin to avoid shell injection)
const personaPath = join(mountPoint, "etc/agent/persona.md");
execFileSync("sudo", ["tee", personaPath], {
input: persona,
stdio: ["pipe", "pipe", "pipe"],
});
// Inject SSH key for debugging access
execFileSync("sudo", ["mkdir", "-p", join(mountPoint, "root/.ssh")], {
@@ -301,14 +300,7 @@ export async function stopAgent(name: string) {
try {
execFileSync(
"ssh",
[
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=3",
"-i", CONFIG.sshKeyPath,
`root@${info.ip}`,
"killall python3 2>/dev/null; sleep 1",
],
[...SSH_OPTS, `root@${info.ip}`, "killall python3 2>/dev/null; sleep 1"],
{ stdio: "pipe", timeout: 5_000 }
);
} catch {
@@ -405,13 +397,6 @@ export async function reloadAgent(
}
if (updates.trigger) configUpdates.trigger = updates.trigger;
// Write updated config as a temp file on the VM via SSH
const sshOpts = [
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=5",
"-i", CONFIG.sshKeyPath,
];
const sshTarget = `root@${info.ip}`;
try {
@@ -419,7 +404,7 @@ export async function reloadAgent(
// Read current config from VM
const currentRaw = execFileSync(
"ssh",
[...sshOpts, sshTarget, "cat /etc/agent/config.json"],
[...SSH_OPTS, sshTarget, "cat /etc/agent/config.json"],
{ encoding: "utf-8", timeout: 10_000 }
);
const current = JSON.parse(currentRaw);
@@ -429,7 +414,7 @@ export async function reloadAgent(
// Write back via stdin
execFileSync(
"ssh",
[...sshOpts, sshTarget, `cat > /etc/agent/config.json`],
[...SSH_OPTS, sshTarget, `cat > /etc/agent/config.json`],
{ input: newConfig, timeout: 10_000 }
);
}
@@ -437,7 +422,7 @@ export async function reloadAgent(
if (updates.persona) {
execFileSync(
"ssh",
[...sshOpts, sshTarget, `cat > /etc/agent/persona.md`],
[...SSH_OPTS, sshTarget, `cat > /etc/agent/persona.md`],
{ input: updates.persona, timeout: 10_000 }
);
}
@@ -445,7 +430,7 @@ export async function reloadAgent(
// Signal agent to reload
execFileSync(
"ssh",
[...sshOpts, sshTarget, "killall -HUP python3"],
[...SSH_OPTS, sshTarget, "killall -HUP python3"],
{ stdio: "pipe", timeout: 10_000 }
);
} catch (err) {