diff --git a/CHANGELOG.md b/CHANGELOG.md index f518bdd691b..8d74a2c8177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security/Node metadata policy: harden node platform classification against Unicode confusables and switch unknown platform defaults to a conservative allowlist that excludes `system.run`/`system.which` unless explicitly allowlisted, preventing metadata canonicalization drift from broadening node command permissions. Thanks @tdjackey for reporting. - Plugins/Discovery precedence: load bundled plugins before auto-discovered global extensions so bundled channel plugins win duplicate-ID resolution by default (explicit `plugins.load.paths` overrides remain highest precedence), with loader regression coverage. Landed from contributor PR #29710 by @Sid-Qin. Thanks @Sid-Qin. - Discord/Reconnect integrity: release Discord message listener lane immediately while preserving serialized handler execution, add HELLO-stall resume-first recovery with bounded fresh-identify fallback after repeated stalls, and extend lifecycle/listener regression coverage for forced reconnect scenarios. Landed from contributor PR #29508 by @cgdusek. Thanks @cgdusek. - Matrix/Conduit compatibility: avoid blocking startup on non-resolving Matrix sync start, preserve startup error propagation, prevent duplicate monitor listener registration, remove unreliable 2-member DM heuristics, accept `!room` IDs without alias resolution, and add matrix monitor/client regression coverage. Landed from contributor PR #31023 by @efe-arv. Thanks @efe-arv. diff --git a/docs/nodes/index.md b/docs/nodes/index.md index 70b1f6cae5f..8d5e599419c 100644 --- a/docs/nodes/index.md +++ b/docs/nodes/index.md @@ -277,6 +277,7 @@ Notes: - `system.run` returns stdout/stderr/exit code in the payload. - `system.notify` respects notification permission state on the macOS app. +- Unrecognized node `platform` / `deviceFamily` metadata uses a conservative default allowlist that excludes `system.run` and `system.which`. If you intentionally need those commands for an unknown platform, add them explicitly via `gateway.nodes.allowCommands`. - `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`. - For shell wrappers (`bash|sh|zsh ... -c/-lc`), request-scoped `--env` values are reduced to an explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`). - For allow-always decisions in allowlist mode, known dispatch wrappers (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically. diff --git a/src/gateway/gateway-misc.test.ts b/src/gateway/gateway-misc.test.ts index e672b05d357..f7adcbf512f 100644 --- a/src/gateway/gateway-misc.test.ts +++ b/src/gateway/gateway-misc.test.ts @@ -365,6 +365,34 @@ describe("resolveNodeCommandAllowlist", () => { expect(allow.has("screen.record")).toBe(true); expect(allow.has("camera.clip")).toBe(false); }); + + it("treats unknown/confusable metadata as fail-safe for system.run defaults", () => { + const allow = resolveNodeCommandAllowlist( + {}, + { + platform: "iPhοne", + deviceFamily: "iPhοne", + }, + ); + + expect(allow.has("system.run")).toBe(false); + expect(allow.has("system.which")).toBe(false); + expect(allow.has("system.notify")).toBe(true); + }); + + it("normalizes dotted-I platform values to iOS classification", () => { + const allow = resolveNodeCommandAllowlist( + {}, + { + platform: "İOS", + deviceFamily: "iPhone", + }, + ); + + expect(allow.has("system.run")).toBe(false); + expect(allow.has("system.which")).toBe(false); + expect(allow.has("device.info")).toBe(true); + }); }); describe("normalizeVoiceWakeTriggers", () => { diff --git a/src/gateway/node-command-policy.ts b/src/gateway/node-command-policy.ts index 01d1b920c7f..1429f71a823 100644 --- a/src/gateway/node-command-policy.ts +++ b/src/gateway/node-command-policy.ts @@ -52,6 +52,12 @@ const SYSTEM_COMMANDS = [ NODE_SYSTEM_NOTIFY_COMMAND, NODE_BROWSER_PROXY_COMMAND, ]; +const UNKNOWN_PLATFORM_COMMANDS = [ + ...CANVAS_COMMANDS, + ...CAMERA_COMMANDS, + ...LOCATION_COMMANDS, + NODE_SYSTEM_NOTIFY_COMMAND, +]; // "High risk" node commands. These can be enabled by explicitly adding them to // `gateway.nodes.allowCommands` (and ensuring they're not blocked by denyCommands). @@ -104,11 +110,19 @@ const PLATFORM_DEFAULTS: Record = { ], linux: [...SYSTEM_COMMANDS], windows: [...SYSTEM_COMMANDS], - unknown: [...CANVAS_COMMANDS, ...CAMERA_COMMANDS, ...LOCATION_COMMANDS, ...SYSTEM_COMMANDS], + // Fail-safe: unknown metadata should not receive host exec defaults. + unknown: [...UNKNOWN_PLATFORM_COMMANDS], }; +function normalizePlatformToken(value?: string): string { + if (typeof value !== "string") { + return ""; + } + return value.trim().normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase(); +} + function normalizePlatformId(platform?: string, deviceFamily?: string): string { - const raw = (platform ?? "").trim().toLowerCase(); + const raw = normalizePlatformToken(platform); if (raw.startsWith("ios")) { return "ios"; } @@ -127,7 +141,7 @@ function normalizePlatformId(platform?: string, deviceFamily?: string): string { if (raw.startsWith("linux")) { return "linux"; } - const family = (deviceFamily ?? "").trim().toLowerCase(); + const family = normalizePlatformToken(deviceFamily); if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) { return "ios"; }