mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 03:30:43 +00:00
fix: add root guard to prevent CLI execution as root (#67478)
Block openclaw CLI from running as root (uid 0) to prevent: - Separate state directory at /root/.openclaw/ - Conflicting systemd user services racing on port 18789 - Root-owned files in the service user state dir (EACCES) The guard runs early in src/entry.ts before any state/config operations. Root-level --help and --version bypass the guard so users can discover the OPENCLAW_ALLOW_ROOT=1 override. Subcommand help paths still enforce the guard since they enter runCli() and resolve state directories. Closes #67478
This commit is contained in:
71
src/cli/root-guard.test.ts
Normal file
71
src/cli/root-guard.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("assertNotRoot", () => {
|
||||
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
||||
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
|
||||
// Save and restore real getuid so we can replace it per test.
|
||||
const realGetuid = process.getuid;
|
||||
|
||||
beforeEach(() => {
|
||||
exitSpy.mockClear();
|
||||
stderrSpy.mockClear();
|
||||
process.getuid = realGetuid;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
exitSpy.mockRestore();
|
||||
stderrSpy.mockRestore();
|
||||
process.getuid = realGetuid;
|
||||
});
|
||||
|
||||
// Use a fresh import each time to avoid module-level caching issues.
|
||||
async function loadAssertNotRoot() {
|
||||
const mod = await import("./root-guard.js");
|
||||
return mod.assertNotRoot;
|
||||
}
|
||||
|
||||
it("exits with code 1 when uid is 0 and no env override", async () => {
|
||||
process.getuid = () => 0;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({});
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("does not exit when uid is 0 and OPENCLAW_ALLOW_ROOT=1", async () => {
|
||||
process.getuid = () => 0;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({ OPENCLAW_ALLOW_ROOT: "1" });
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not exit when uid is non-zero", async () => {
|
||||
process.getuid = () => 1000;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({});
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not exit when getuid is undefined (Windows)", async () => {
|
||||
process.getuid = undefined as unknown as typeof process.getuid;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({});
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("error message mentions OPENCLAW_ALLOW_ROOT", async () => {
|
||||
process.getuid = () => 0;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({});
|
||||
const output = stderrSpy.mock.calls.map((c) => String(c[0])).join("");
|
||||
expect(output).toContain("OPENCLAW_ALLOW_ROOT");
|
||||
});
|
||||
|
||||
it("error message mentions running as a non-root user", async () => {
|
||||
process.getuid = () => 0;
|
||||
const assertNotRoot = await loadAssertNotRoot();
|
||||
assertNotRoot({});
|
||||
const output = stderrSpy.mock.calls.map((c) => String(c[0])).join("");
|
||||
expect(output).toContain("non-root user");
|
||||
});
|
||||
});
|
||||
34
src/cli/root-guard.ts
Normal file
34
src/cli/root-guard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import process from "node:process";
|
||||
|
||||
/**
|
||||
* Block CLI execution when running as root (uid 0) unless explicitly opted in.
|
||||
*
|
||||
* Running as root causes:
|
||||
* - Separate state dir (/root/.openclaw/ vs /home/<user>/.openclaw/)
|
||||
* - Conflicting systemd user services (port 18789 race)
|
||||
* - Root-owned files in the service user's state dir (EACCES)
|
||||
*/
|
||||
export function assertNotRoot(env: NodeJS.ProcessEnv = process.env): void {
|
||||
if (typeof process.getuid !== "function") {
|
||||
return;
|
||||
}
|
||||
if (process.getuid() !== 0) {
|
||||
return;
|
||||
}
|
||||
if (env.OPENCLAW_ALLOW_ROOT === "1") {
|
||||
return;
|
||||
}
|
||||
process.stderr.write(
|
||||
"[openclaw] Refusing to run as root.\n" +
|
||||
"\n" +
|
||||
"Running the CLI as root causes:\n" +
|
||||
" - A separate state directory under /root/.openclaw/ instead of the service user's\n" +
|
||||
" - Conflicting systemd user services that race on port 18789\n" +
|
||||
" - Root-owned files in the service user's state dir (EACCES errors)\n" +
|
||||
"\n" +
|
||||
"Run as a non-root user (e.g. su - <service-user>),\n" +
|
||||
"or override this check:\n" +
|
||||
" OPENCLAW_ALLOW_ROOT=1 openclaw ...\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
10
src/entry.ts
10
src/entry.ts
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isRootHelpInvocation } from "./cli/argv.js";
|
||||
import { isRootHelpInvocation, isRootVersionInvocation } from "./cli/argv.js";
|
||||
import { assertNotRoot } from "./cli/root-guard.js";
|
||||
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
|
||||
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
|
||||
@@ -91,6 +92,13 @@ if (
|
||||
ensureOpenClawExecMarkerOnProcess();
|
||||
installProcessWarningFilter();
|
||||
normalizeEnv();
|
||||
|
||||
// Block root execution early, before any state/config operations.
|
||||
// Allow --help and --version so users can still discover the override env var.
|
||||
if (!isRootHelpInvocation(process.argv) && !isRootVersionInvocation(process.argv)) {
|
||||
assertNotRoot();
|
||||
}
|
||||
|
||||
enableOpenClawCompileCache({
|
||||
installRoot,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user