diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index f4d4e3935..4d3073a06 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -46,6 +46,29 @@ const nim = require("./nim"); const onboardSession = require("./onboard-session"); const policies = require("./policies"); const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight"); + +/** + * Create a temp file inside a directory with a cryptographically random name. + * Uses fs.mkdtempSync (OS-level mkdtemp) to avoid predictable filenames that + * could be exploited via symlink attacks on shared /tmp. + * Ref: https://github.com/NVIDIA/NemoClaw/issues/1093 + */ +function secureTempFile(prefix, ext = "") { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`)); + return path.join(dir, `${prefix}${ext}`); +} + +/** + * Safely remove a mkdtemp-created directory. Guards against accidentally + * deleting the system temp root if a caller passes os.tmpdir() itself. + */ +function cleanupTempDir(filePath, expectedPrefix) { + const parentDir = path.dirname(filePath); + if (parentDir !== os.tmpdir() && path.basename(parentDir).startsWith(`${expectedPrefix}-`)) { + fs.rmSync(parentDir, { recursive: true, force: true }); + } +} + const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; const USE_COLOR = !process.env.NO_COLOR && !!process.stdout.isTTY; const DIM = USE_COLOR ? "\x1b[2m" : ""; @@ -677,7 +700,7 @@ function getProbeRecovery(probe, options = {}) { // eslint-disable-next-line complexity function runCurlProbe(argv) { - const bodyFile = path.join(os.tmpdir(), `nemoclaw-curl-probe-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); + const bodyFile = secureTempFile("nemoclaw-curl-probe", ".json"); try { const args = [...argv]; const url = args.pop(); @@ -725,7 +748,7 @@ function runCurlProbe(argv) { message: summarizeCurlFailure(error?.status || 1, error?.message || String(error)), }; } finally { - fs.rmSync(bodyFile, { force: true }); + cleanupTempDir(bodyFile, "nemoclaw-curl-probe"); } } @@ -907,9 +930,8 @@ function isOpenclawReady(sandboxName) { return Boolean(fetchGatewayAuthTokenFromSandbox(sandboxName)); } -function writeSandboxConfigSyncFile(script, tmpDir = os.tmpdir()) { - const dir = fs.mkdtempSync(path.join(tmpDir, "nemoclaw-sync-")); - const scriptFile = path.join(dir, "sync.sh"); +function writeSandboxConfigSyncFile(script) { + const scriptFile = secureTempFile("nemoclaw-sync", ".sh"); fs.writeFileSync(scriptFile, `${script}\n`, { mode: 0o600 }); return scriptFile; } @@ -2901,7 +2923,7 @@ async function setupOpenclaw(sandboxName, model, provider) { { stdio: ["ignore", "ignore", "inherit"] } ); } finally { - fs.rmSync(path.dirname(scriptFile), { recursive: true, force: true }); + cleanupTempDir(scriptFile, "nemoclaw-sync"); } } diff --git a/test/onboard.test.js b/test/onboard.test.js index 96dc1e0e9..43c3bfc42 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -452,17 +452,23 @@ 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-")); + const scriptFile = writeSandboxConfigSyncFile("echo test"); try { - const scriptFile = writeSandboxConfigSyncFile("echo test", tmpDir); - expect(scriptFile).toMatch(/nemoclaw-sync-.*[/\\]sync\.sh$/); + expect(scriptFile).toMatch(/nemoclaw-sync.*\.sh$/); expect(fs.readFileSync(scriptFile, "utf8")).toBe("echo test\n"); + // Verify the file lives inside a mkdtemp-created directory (not directly in /tmp) + const parentDir = path.dirname(scriptFile); + expect(parentDir).not.toBe(os.tmpdir()); + expect(parentDir).toContain("nemoclaw-sync"); if (process.platform !== "win32") { const stat = fs.statSync(scriptFile); expect(stat.mode & 0o777).toBe(0o600); } } finally { - fs.rmSync(tmpDir, { recursive: true, force: true }); + const parentDir = path.dirname(scriptFile); + if (parentDir !== os.tmpdir() && path.basename(parentDir).startsWith("nemoclaw-sync-")) { + fs.rmSync(parentDir, { recursive: true, force: true }); + } } });