From 9b69aadca4b21f3299af6e118a34c3147805df2c Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 1 Jun 2026 22:00:49 +0200 Subject: [PATCH] Gate edge-agent shell actions behind local opt-in --- services/edge-agent/dist/agent.mjs | 20 +++++++++ services/edge-agent/test/agent.test.mjs | 57 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/services/edge-agent/dist/agent.mjs b/services/edge-agent/dist/agent.mjs index 317c3059..9a49e1e9 100644 --- a/services/edge-agent/dist/agent.mjs +++ b/services/edge-agent/dist/agent.mjs @@ -148,6 +148,10 @@ function buildTransportHeartbeatState(brokerState = {}) { }; } +function isShellAccessEnabled(config = {}) { + return config.enableShellAccess === true; +} + function normalizeBrokerBaseUrl(value) { const trimmed = String(value || "").trim().replace(/\/+$/, ""); if (trimmed === "") { @@ -1561,6 +1565,10 @@ export async function processPolledShellAction(config, action, shell, fetchImpl } try { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } + if (actionType === "OPEN") { await shell.open(payload); } else if (actionType === "INPUT") { @@ -1691,18 +1699,30 @@ function createBrokerBridge({ } if (message.type === "OPEN_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } await shell.open(message.payload || {}); return; } if (message.type === "SHELL_INPUT") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.input(message.payload || {}); return; } if (message.type === "RESIZE_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.resize(message.payload || {}); return; } if (message.type === "CLOSE_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.close(message.payload || {}); } } catch { diff --git a/services/edge-agent/test/agent.test.mjs b/services/edge-agent/test/agent.test.mjs index 4e8dd7bc..0ee07822 100644 --- a/services/edge-agent/test/agent.test.mjs +++ b/services/edge-agent/test/agent.test.mjs @@ -16,6 +16,7 @@ import { getRelayStatus, loadConfig, parseCliArgs, + processPolledShellAction, processPolledCommand, runCli, runUpdate, @@ -485,6 +486,7 @@ test("startAgent reports API polling metadata, executes polled commands, and upl commandPollRetryDelayMs: 5, shellActionPollTimeoutSeconds: 0, shellActionPollRetryDelayMs: 5, + enableShellAccess: true, })); const heartbeats = []; @@ -742,6 +744,61 @@ test("startAgent reports API polling metadata, executes polled commands, and upl } }); +test("processPolledShellAction denies shell access when locally disabled", async () => { + const submissions = []; + const fakeFetch = async (url, options = {}) => { + if (/\/shell-actions\/\d+\/result$/.test(String(url))) { + submissions.push({ url, body: JSON.parse(options.body) }); + return { + ok: true, + async json() { + return { data: { acknowledged: true } }; + }, + }; + } + + throw new Error(`Unexpected URL: ${url}`); + }; + + const shell = { + async open() { + throw new Error("should not run"); + }, + input() { + throw new Error("should not run"); + }, + resize() { + throw new Error("should not run"); + }, + close() { + throw new Error("should not run"); + }, + }; + + const result = await processPolledShellAction( + { + apiUrl: "https://api.example.test", + gatewayId: 42, + agentToken: "agent-token", + enableShellAccess: false, + }, + { + id: 501, + actionType: "OPEN", + payload: { + sessionId: 44, + }, + }, + shell, + fakeFetch + ); + + assert.equal(result.ok, false); + assert.match(result.error, /Shell access is disabled/); + assert.equal(submissions.length, 1); + assert.equal(submissions[0].body.ok, false); +}); + test("status helpers report config without exposing the agent token", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-status-")); const configPath = path.join(tempDir, "config.json");