From 1763732fa98f40fe73a781babaa20c2cf9bf92be Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Mon, 30 Mar 2026 09:11:41 -0500 Subject: [PATCH 1/2] fix(onboard): replace predictable temp filenames with mkdtempSync Probe functions and writeSandboxConfigSyncFile use Date.now() and Math.random() to construct temp filenames in os.tmpdir(). These are predictable, allowing a local attacker to race the file creation with a symlink and redirect curl output (which may contain API responses) to an attacker-controlled path. Replace all 6 sites with fs.mkdtempSync() which creates a directory with a cryptographically random suffix and restrictive permissions. This matches the pattern already used at lines 1764 and 2680 in the same file. Signed-off-by: latenighthackathon --- bin/lib/onboard.js | 32 +++++++++++++++++++------------- test/onboard.test.js | 6 ++++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index d5606ae51..dafbe6309 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -523,8 +523,9 @@ function isOpenclawReady(sandboxName) { return Boolean(fetchGatewayAuthTokenFromSandbox(sandboxName)); } -function writeSandboxConfigSyncFile(script, tmpDir = os.tmpdir(), now = Date.now()) { - const scriptFile = path.join(tmpDir, `nemoclaw-sync-${now}.sh`); +function writeSandboxConfigSyncFile(script, tmpDir = os.tmpdir()) { + const dir = fs.mkdtempSync(path.join(tmpDir, "nemoclaw-sync-")); + const scriptFile = path.join(dir, "sync.sh"); fs.writeFileSync(scriptFile, `${script}\n`, { mode: 0o600 }); return scriptFile; } @@ -663,7 +664,8 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) { const failures = []; for (const probe of probes) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-probe-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-probe-")); + const bodyFile = path.join(probeDir, "body.json"); try { const cmd = [ "curl -sS", @@ -695,7 +697,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) { message: summarizeProbeError(body, status || result.status || 0), }); } finally { - fs.rmSync(bodyFile, { force: true }); + fs.rmSync(probeDir, { recursive: true, force: true }); } } @@ -707,7 +709,8 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) { } function probeAnthropicEndpoint(endpointUrl, model, apiKey) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-anthropic-probe-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-anthropic-probe-")); + const bodyFile = path.join(probeDir, "body.json"); try { const cmd = [ "curl -sS", @@ -749,7 +752,7 @@ function probeAnthropicEndpoint(endpointUrl, model, apiKey) { ], }; } finally { - fs.rmSync(bodyFile, { force: true }); + fs.rmSync(probeDir, { recursive: true, force: true }); } } @@ -853,7 +856,8 @@ async function validateCustomAnthropicSelection(label, endpointUrl, model, crede } function fetchNvidiaEndpointModels(apiKey) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-nvidia-models-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-nvidia-models-")); + const bodyFile = path.join(probeDir, "body.json"); try { const cmd = [ "curl -sS", @@ -885,7 +889,7 @@ function fetchNvidiaEndpointModels(apiKey) { } catch (error) { return { ok: false, message: error.message || String(error) }; } finally { - fs.rmSync(bodyFile, { force: true }); + fs.rmSync(probeDir, { recursive: true, force: true }); } } @@ -907,7 +911,8 @@ function validateNvidiaEndpointModel(model, apiKey) { } function fetchOpenAiLikeModels(endpointUrl, apiKey) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-openai-models-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openai-models-")); + const bodyFile = path.join(probeDir, "body.json"); try { const cmd = [ "curl -sS", @@ -938,12 +943,13 @@ function fetchOpenAiLikeModels(endpointUrl, apiKey) { } catch (error) { return { ok: false, status: 0, message: error.message || String(error) }; } finally { - fs.rmSync(bodyFile, { force: true }); + fs.rmSync(probeDir, { recursive: true, force: true }); } } function fetchAnthropicModels(endpointUrl, apiKey) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-anthropic-models-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-anthropic-models-")); + const bodyFile = path.join(probeDir, "body.json"); try { const cmd = [ "curl -sS", @@ -975,7 +981,7 @@ function fetchAnthropicModels(endpointUrl, apiKey) { } catch (error) { return { ok: false, status: 0, message: error.message || String(error) }; } finally { - fs.rmSync(bodyFile, { force: true }); + fs.rmSync(probeDir, { recursive: true, force: true }); } } @@ -2367,7 +2373,7 @@ async function setupOpenclaw(sandboxName, model, provider) { { stdio: ["ignore", "ignore", "inherit"] } ); } finally { - fs.unlinkSync(scriptFile); + fs.rmSync(path.dirname(scriptFile), { recursive: true, force: true }); } } diff --git a/test/onboard.test.js b/test/onboard.test.js index 16b7e5453..cf1e9eea9 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -428,9 +428,11 @@ describe("onboard helpers", () => { it("writes sandbox sync scripts to a temp file for stdin redirection", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-test-")); try { - const scriptFile = writeSandboxConfigSyncFile("echo test", tmpDir, 1234); - expect(scriptFile).toBe(path.join(tmpDir, "nemoclaw-sync-1234.sh")); + const scriptFile = writeSandboxConfigSyncFile("echo test", tmpDir); + expect(scriptFile).toMatch(/nemoclaw-sync-.*\/sync\.sh$/); expect(fs.readFileSync(scriptFile, "utf8")).toBe("echo test\n"); + const stat = fs.statSync(scriptFile); + expect(stat.mode & 0o777).toBe(0o600); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } From 43402afff1c42cb7672f780f9bcf35cf1af9de6f Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Mon, 30 Mar 2026 11:17:46 -0500 Subject: [PATCH 2/2] fix(test): handle cross-platform paths in writeSandboxConfigSyncFile test Accept both forward and back slashes in the path regex and skip the Unix file permission assertion on Windows where mode bits are not enforced. Signed-off-by: latenighthackathon --- test/onboard.test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/onboard.test.js b/test/onboard.test.js index cf1e9eea9..aa26e5b23 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -429,10 +429,12 @@ describe("onboard helpers", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-test-")); try { const scriptFile = writeSandboxConfigSyncFile("echo test", tmpDir); - expect(scriptFile).toMatch(/nemoclaw-sync-.*\/sync\.sh$/); + expect(scriptFile).toMatch(/nemoclaw-sync-.*[/\\]sync\.sh$/); expect(fs.readFileSync(scriptFile, "utf8")).toBe("echo test\n"); - const stat = fs.statSync(scriptFile); - expect(stat.mode & 0o777).toBe(0o600); + if (process.platform !== "win32") { + const stat = fs.statSync(scriptFile); + expect(stat.mode & 0o777).toBe(0o600); + } } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); }