diff --git a/CHANGELOG.md b/CHANGELOG.md index d9904bcf2a7..bc7ce6e37f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai - Models/Codex: include `apiKey` in the codex provider catalog output so the Pi ModelRegistry validator no longer rejects the entry and silently drops all custom models from every provider in `models.json`. (#66180) Thanks @hoyyeva. - Slack/interactions: apply the configured global `allowFrom` owner allowlist to channel block-action and modal interactive events, require an expected sender id for cross-verification, and reject ambiguous channel types so interactive triggers can no longer bypass the documented allowlist intent in channels without a `users` list. Open-by-default behavior is preserved when no allowlists are configured. (#66028) Thanks @eleqtrizit. - Media-understanding/attachments: fail closed when a local attachment path cannot be canonically resolved via `realpath`, so a `realpath` error can no longer downgrade the canonical-roots allowlist check to a non-canonical comparison; attachments that also have a URL still fall back to the network fetch path. (#66022) Thanks @eleqtrizit. +- Agents/gateway-tool: reject `config.patch` and `config.apply` calls from the model-facing gateway tool when they would newly enable any flag enumerated by `openclaw security audit` (for example `dangerouslyDisableDeviceAuth`, `allowInsecureAuth`, `dangerouslyAllowHostHeaderOriginFallback`, `hooks.gmail.allowUnsafeExternalContent`, `tools.exec.applyPatch.workspaceOnly: false`); already-enabled flags pass through unchanged so non-dangerous edits in the same patch still apply, and direct authenticated operator RPC behavior is unchanged. (#62006) Thanks @eleqtrizit. ## 2026.4.14-beta.1 diff --git a/src/agents/openclaw-gateway-tool.test.ts b/src/agents/openclaw-gateway-tool.test.ts index fc050cd9ea5..16bcebd953a 100644 --- a/src/agents/openclaw-gateway-tool.test.ts +++ b/src/agents/openclaw-gateway-tool.test.ts @@ -397,6 +397,149 @@ describe("gateway tool", () => { ); }); + it("rejects config.patch that enables dangerouslyDisableDeviceAuth", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-device-auth", { + action: "config.patch", + raw: "{ gateway: { controlUi: { dangerouslyDisableDeviceAuth: true } } }", + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("rejects config.patch that enables allowUnsafeExternalContent on gmail hooks", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-gmail", { + action: "config.patch", + raw: "{ hooks: { gmail: { allowUnsafeExternalContent: true } } }", + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("rejects config.patch that weakens applyPatch.workspaceOnly", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-workspace", { + action: "config.patch", + raw: "{ tools: { exec: { applyPatch: { workspaceOnly: false } } } }", + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("rejects config.patch that enables allowInsecureAuth on control UI", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-insecure-auth", { + action: "config.patch", + raw: "{ gateway: { controlUi: { allowInsecureAuth: true } } }", + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("rejects config.patch that enables dangerouslyAllowHostHeaderOriginFallback", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-origin-fallback", { + action: "config.patch", + raw: "{ gateway: { controlUi: { dangerouslyAllowHostHeaderOriginFallback: true } } }", + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("allows config.patch that does not enable any dangerous flag", async () => { + const sessionKey = "agent:main:whatsapp:dm:+15555550123"; + const tool = requireGatewayTool(sessionKey); + + const raw = '{ channels: { telegram: { groups: { "*": { requireMention: false } } } } }'; + await tool.execute("call-safe-patch", { + action: "config.patch", + raw, + }); + + expect(callGatewayTool).toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.objectContaining({ raw: raw.trim() }), + ); + }); + + it("allows config.patch when a dangerous flag is already enabled and stays enabled", async () => { + vi.mocked(callGatewayTool).mockImplementationOnce(async (method: string) => { + if (method === "config.get") { + return { + hash: "hash-1", + config: { + tools: { exec: { ask: "on-miss", security: "allowlist" } }, + hooks: { gmail: { allowUnsafeExternalContent: true } }, + }, + }; + } + return { ok: true }; + }); + const sessionKey = "agent:main:whatsapp:dm:+15555550123"; + const tool = requireGatewayTool(sessionKey); + + const raw = + '{ hooks: { gmail: { allowUnsafeExternalContent: true } }, agents: { defaults: { workspace: "~/test" } } }'; + await tool.execute("call-keep-dangerous", { + action: "config.patch", + raw, + }); + + expect(callGatewayTool).toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.objectContaining({ raw: raw.trim() }), + ); + }); + + it("rejects config.apply that introduces a dangerous flag", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-dangerous-apply", { + action: "config.apply", + raw: '{ tools: { exec: { ask: "on-miss", security: "allowlist", applyPatch: { workspaceOnly: false } } } }', + }), + ).rejects.toThrow("cannot enable dangerous config flags"); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.apply", + expect.any(Object), + expect.anything(), + ); + }); + it("passes update.run through gateway call", async () => { const sessionKey = "agent:main:whatsapp:dm:+15555550123"; const tool = requireGatewayTool(sessionKey); diff --git a/src/agents/tools/gateway-tool.ts b/src/agents/tools/gateway-tool.ts index 913ea38fd2d..52ae244405f 100644 --- a/src/agents/tools/gateway-tool.ts +++ b/src/agents/tools/gateway-tool.ts @@ -12,6 +12,7 @@ import { } from "../../infra/restart-sentinel.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { collectEnabledInsecureOrDangerousFlags } from "../../security/dangerous-config-flags.js"; import { normalizeOptionalString, readStringValue } from "../../shared/string-coerce.js"; import { stringEnum } from "../schema/typebox.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; @@ -113,12 +114,24 @@ function assertGatewayConfigMutationAllowed(params: { getValueAtPath(nextConfig, path), ), ); - if (changedProtectedPaths.length === 0) { - return; + if (changedProtectedPaths.length > 0) { + throw new Error( + `gateway ${params.action} cannot change protected config paths: ${changedProtectedPaths.join(", ")}`, + ); } - throw new Error( - `gateway ${params.action} cannot change protected config paths: ${changedProtectedPaths.join(", ")}`, + + // Block writes that newly enable any dangerous config flag. + // Uses the same flag enumeration as `openclaw security audit`. + const currentFlags = new Set( + collectEnabledInsecureOrDangerousFlags(params.currentConfig as OpenClawConfig), ); + const nextFlags = collectEnabledInsecureOrDangerousFlags(nextConfig as OpenClawConfig); + const newlyEnabled = nextFlags.filter((f) => !currentFlags.has(f)); + if (newlyEnabled.length > 0) { + throw new Error( + `gateway ${params.action} cannot enable dangerous config flags: ${newlyEnabled.join(", ")}`, + ); + } } const GATEWAY_ACTIONS = [