From 18f12c625118bdbfe439961789f8c2d52a568264 Mon Sep 17 00:00:00 2001 From: Alan Pena Date: Fri, 27 Mar 2026 14:22:17 -0400 Subject: [PATCH] fix(scripts): make config integrity check fatal in non-root mode In nemoclaw-start.sh, the non-root code path caught verify_config_integrity failures and continued with a warning, bypassing the security model that protects openclaw.json from tampering. The root code path correctly treated the check as fatal (exits under set -euo pipefail). The integrity check is now fatal in both code paths. If the config hash doesn't match, the sandbox refuses to start regardless of whether it's running as root or non-root. Adds regression tests verifying: - The non-root block exits on integrity failure - No code path bypasses verify_config_integrity Fixes #1013 --- scripts/nemoclaw-start.sh | 3 ++- test/nemoclaw-start.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index db1e19e9b..526409276 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -184,7 +184,8 @@ if [ "$(id -u)" -ne 0 ]; then echo "[gateway] Running as non-root (uid=$(id -u)) — privilege separation disabled" export HOME=/sandbox if ! verify_config_integrity; then - echo "[SECURITY WARNING] Config integrity check failed — proceeding anyway (non-root mode)" + echo "[SECURITY] Config integrity check failed — refusing to start (non-root mode)" + exit 1 fi write_auth_profile diff --git a/test/nemoclaw-start.test.js b/test/nemoclaw-start.test.js index 246b67cb9..046120fd5 100644 --- a/test/nemoclaw-start.test.js +++ b/test/nemoclaw-start.test.js @@ -16,3 +16,31 @@ describe("nemoclaw-start non-root fallback", () => { expect(src).toMatch(/nohup "\$OPENCLAW" gateway run >\/tmp\/gateway\.log 2>&1 &/); }); }); + +describe("config integrity check", () => { + const src = fs.readFileSync(START_SCRIPT, "utf-8"); + + it("exits on integrity failure in non-root mode", () => { + // Extract the outer non-root block. The trailing \nfi\n targets the + // closing "fi" at column 0 — inner "fi" lines are indented (e.g. " fi") + // so [\s\S]*? stops at the first unindented fi, selecting only the outer + // block. This relies on consistent indentation in nemoclaw-start.sh. + const nonRootMatch = src.match( + /if \[ "\$\(id -u\)" -ne 0 \]; then\n([\s\S]*?)\nfi\n/ + ); + expect(nonRootMatch).not.toBeNull(); + const nonRootBlock = nonRootMatch[1]; + + // The block must NOT contain "proceeding anyway" — that was the old bypass + expect(nonRootBlock).not.toMatch(/proceeding anyway/i); + + // The block must exit on integrity failure + expect(nonRootBlock).toMatch(/verify_config_integrity/); + expect(nonRootBlock).toMatch(/exit 1/); + }); + + it("does not bypass verify_config_integrity in any code path", () => { + // No line should catch and ignore a verify_config_integrity failure + expect(src).not.toMatch(/verify_config_integrity[\s\S]*?proceeding anyway/); + }); +});