Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ 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" : "";
Expand Down Expand Up @@ -525,9 +547,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;
}
Expand Down Expand Up @@ -666,8 +687,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) {

const failures = [];
for (const probe of probes) {
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-probe-"));
const bodyFile = path.join(probeDir, "body.json");
const bodyFile = secureTempFile("nemoclaw-probe", ".json");
try {
const cmd = [
"curl -sS",
Expand Down Expand Up @@ -700,7 +720,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) {
message: summarizeProbeError(body, status || result.status || 0),
});
} finally {
fs.rmSync(probeDir, { recursive: true, force: true });
cleanupTempDir(bodyFile, "nemoclaw-probe");
}
}

Expand All @@ -712,8 +732,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey) {
}

function probeAnthropicEndpoint(endpointUrl, model, apiKey) {
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-anthropic-probe-"));
const bodyFile = path.join(probeDir, "body.json");
const bodyFile = secureTempFile("nemoclaw-anthropic-probe", ".json");
try {
const cmd = [
"curl -sS",
Expand Down Expand Up @@ -756,7 +775,7 @@ function probeAnthropicEndpoint(endpointUrl, model, apiKey) {
],
};
} finally {
fs.rmSync(probeDir, { recursive: true, force: true });
cleanupTempDir(bodyFile, "nemoclaw-anthropic-probe");
}
}

Expand Down Expand Up @@ -860,8 +879,7 @@ async function validateCustomAnthropicSelection(label, endpointUrl, model, crede
}

function fetchNvidiaEndpointModels(apiKey) {
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-nvidia-models-"));
const bodyFile = path.join(probeDir, "body.json");
const bodyFile = secureTempFile("nemoclaw-nvidia-models", ".json");
try {
const cmd = [
"curl -sS",
Expand Down Expand Up @@ -894,7 +912,7 @@ function fetchNvidiaEndpointModels(apiKey) {
} catch (error) {
return { ok: false, message: error.message || String(error) };
} finally {
fs.rmSync(probeDir, { recursive: true, force: true });
cleanupTempDir(bodyFile, "nemoclaw-nvidia-models");
}
}

Expand All @@ -916,8 +934,7 @@ function validateNvidiaEndpointModel(model, apiKey) {
}

function fetchOpenAiLikeModels(endpointUrl, apiKey) {
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openai-models-"));
const bodyFile = path.join(probeDir, "body.json");
const bodyFile = secureTempFile("nemoclaw-openai-models", ".json");
try {
const cmd = [
"curl -sS",
Expand Down Expand Up @@ -949,13 +966,12 @@ function fetchOpenAiLikeModels(endpointUrl, apiKey) {
} catch (error) {
return { ok: false, status: 0, message: error.message || String(error) };
} finally {
fs.rmSync(probeDir, { recursive: true, force: true });
cleanupTempDir(bodyFile, "nemoclaw-openai-models");
}
}

function fetchAnthropicModels(endpointUrl, apiKey) {
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-anthropic-models-"));
const bodyFile = path.join(probeDir, "body.json");
const bodyFile = secureTempFile("nemoclaw-anthropic-models", ".json");
try {
const cmd = [
"curl -sS",
Expand Down Expand Up @@ -988,7 +1004,7 @@ function fetchAnthropicModels(endpointUrl, apiKey) {
} catch (error) {
return { ok: false, status: 0, message: error.message || String(error) };
} finally {
fs.rmSync(probeDir, { recursive: true, force: true });
cleanupTempDir(bodyFile, "nemoclaw-anthropic-models");
}
}

Expand Down Expand Up @@ -2478,7 +2494,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");
}
}

Expand Down
14 changes: 10 additions & 4 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,17 +426,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 });
}
}
});

Expand Down