fix(security): harden runtime command override gating

This commit is contained in:
Peter Steinberger
2026-02-21 12:49:45 +01:00
parent cb84c537f4
commit fbb79d4013
12 changed files with 149 additions and 13 deletions

View File

@@ -62,6 +62,20 @@ describe("commands registry", () => {
expect(nativeDisabled.find((spec) => spec.name === "debug")).toBeFalsy();
});
it("does not enable restricted commands from inherited flags", () => {
const inheritedCommands = Object.create({
config: true,
debug: true,
bash: true,
}) as Record<string, unknown>;
const commands = listChatCommandsForConfig({
commands: inheritedCommands as never,
});
expect(commands.find((spec) => spec.key === "config")).toBeFalsy();
expect(commands.find((spec) => spec.key === "debug")).toBeFalsy();
expect(commands.find((spec) => spec.key === "bash")).toBeFalsy();
});
it("appends skill commands when provided", () => {
const skillCommands = [
{

View File

@@ -1,6 +1,7 @@
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import type { SkillCommandSpec } from "../agents/skills.js";
import { isCommandFlagEnabled } from "../config/commands.js";
import type { OpenClawConfig } from "../config/types.js";
import { escapeRegExp } from "../utils.js";
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
@@ -96,13 +97,13 @@ export function listChatCommands(params?: {
export function isCommandEnabled(cfg: OpenClawConfig, commandKey: string): boolean {
if (commandKey === "config") {
return cfg.commands?.config === true;
return isCommandFlagEnabled(cfg, "config");
}
if (commandKey === "debug") {
return cfg.commands?.debug === true;
return isCommandFlagEnabled(cfg, "debug");
}
if (commandKey === "bash") {
return cfg.commands?.bash === true;
return isCommandFlagEnabled(cfg, "bash");
}
return true;
}

View File

@@ -3,6 +3,7 @@ import { getFinishedSession, getSession, markExited } from "../../agents/bash-pr
import { createExecTool } from "../../agents/bash-tools.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import { killProcessTree } from "../../agents/shell-utils.js";
import { isCommandFlagEnabled } from "../../config/commands.js";
import type { OpenClawConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { clampInt } from "../../utils.js";
@@ -186,7 +187,7 @@ export async function handleBashChatCommand(params: {
failures: Array<{ gate: string; key: string }>;
};
}): Promise<ReplyPayload> {
if (params.cfg.commands?.bash !== true) {
if (!isCommandFlagEnabled(params.cfg, "bash")) {
return {
text: "⚠️ bash is disabled. Set commands.bash=true to enable. Docs: https://docs.openclaw.ai/tools/slash-commands#config",
};

View File

@@ -3,6 +3,7 @@ import { resolveChannelConfigWrites } from "../../channels/plugins/config-writes
import { listPairingChannels } from "../../channels/plugins/pairing.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import { normalizeChannelId } from "../../channels/registry.js";
import { isCommandFlagEnabled } from "../../config/commands.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
readConfigFileSnapshot,
@@ -519,7 +520,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
return { shouldContinue: false, reply: { text: lines.join("\n") } };
}
if (params.cfg.commands?.config !== true) {
if (!isCommandFlagEnabled(params.cfg, "config")) {
return {
shouldContinue: false,
reply: { text: "⚠️ /allowlist edits are disabled. Set commands.config=true to enable." },

View File

@@ -1,5 +1,6 @@
import { resolveChannelConfigWrites } from "../../channels/plugins/config-writes.js";
import { normalizeChannelId } from "../../channels/registry.js";
import { isCommandFlagEnabled } from "../../config/commands.js";
import {
getConfigValueAtPath,
parseConfigPath,
@@ -36,7 +37,7 @@ export const handleConfigCommand: CommandHandler = async (params, allowTextComma
);
return { shouldContinue: false };
}
if (params.cfg.commands?.config !== true) {
if (!isCommandFlagEnabled(params.cfg, "config")) {
return {
shouldContinue: false,
reply: {
@@ -190,7 +191,7 @@ export const handleDebugCommand: CommandHandler = async (params, allowTextComman
);
return { shouldContinue: false };
}
if (params.cfg.commands?.debug !== true) {
if (!isCommandFlagEnabled(params.cfg, "debug")) {
return {
shouldContinue: false,
reply: {

View File

@@ -186,6 +186,30 @@ describe("handleCommands gating", () => {
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/debug is disabled");
});
it("does not enable gated commands from inherited command flags", async () => {
const inheritedCommands = Object.create({
bash: true,
config: true,
debug: true,
}) as Record<string, unknown>;
const cfg = {
commands: inheritedCommands as never,
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const bashResult = await handleCommands(buildParams("/bash echo hi", cfg));
expect(bashResult.shouldContinue).toBe(false);
expect(bashResult.reply?.text).toContain("bash is disabled");
const configResult = await handleCommands(buildParams("/config show", cfg));
expect(configResult.shouldContinue).toBe(false);
expect(configResult.reply?.text).toContain("/config is disabled");
const debugResult = await handleCommands(buildParams("/debug show", cfg));
expect(debugResult.shouldContinue).toBe(false);
expect(debugResult.reply?.text).toContain("/debug is disabled");
});
});
describe("/approve command", () => {

View File

@@ -11,6 +11,7 @@ import { resolveSandboxRuntimeStatus } from "../agents/sandbox.js";
import type { SkillCommandSpec } from "../agents/skills.js";
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../agents/usage.js";
import { resolveChannelModelOverride } from "../channels/model-overrides.js";
import { isCommandFlagEnabled } from "../config/commands.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveMainSessionKey,
@@ -688,10 +689,10 @@ export function buildHelpMessage(cfg?: OpenClawConfig): string {
lines.push("");
const optionParts = ["/think <level>", "/model <id>", "/verbose on|off"];
if (cfg?.commands?.config === true) {
if (isCommandFlagEnabled(cfg, "config")) {
optionParts.push("/config");
}
if (cfg?.commands?.debug === true) {
if (isCommandFlagEnabled(cfg, "debug")) {
optionParts.push("/debug");
}
lines.push("Options");