diff --git a/CHANGELOG.md b/CHANGELOG.md index e02796b51c2..c3817d3fd02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Docs: https://docs.openclaw.ai - Onboard/wizard: simplify the security disclaimer copy (drop the yellow banner and warning icon in favor of plain-prose paragraphs), and flip remaining onboarding pickers with long dynamic option lists to searchable autocompletes (search provider, plugin configure, model provider filter). - Ollama/onboard: populate the cloud-only model list from `ollama.com/api/tags` so `openclaw onboard` reflects the live cloud catalog instead of a static three-model seed; cap the discovered list at 500 and fall back to the previous hardcoded suggestions when ollama.com is unreachable or returns no models. (#68463) Thanks @BruceMacD. +### Fixes + +- Auth/commands: require owner identity (an owner-candidate match or internal `operator.admin`) for owner-enforced commands instead of treating wildcard channel `allowFrom` or empty owner-candidate lists as sufficient, so non-owner senders can no longer reach owner-only commands through a permissive fallback when `enforceOwnerForCommands=true` and `commands.ownerAllowFrom` is unset. (#69774) Thanks @drobison00. + ## 2026.4.20 ### Changes diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index c7be1134d80..4c7394bf8e5 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -706,9 +706,7 @@ export function resolveCommandAuthorization(params: { ? true : ownerAllowlistConfigured ? senderIsOwner - : ownerState.allowAll || - ownerState.ownerCandidatesForCommands.length === 0 || - Boolean(matchedCommandOwner); + : senderIsOwnerByScope || Boolean(matchedCommandOwner); const isAuthorizedSender = resolveCommandSenderAuthorization({ commandAuthorized, isOwnerForCommands, diff --git a/src/auto-reply/command-control.test.ts b/src/auto-reply/command-control.test.ts index 77a3eef849f..010e082f30a 100644 --- a/src/auto-reply/command-control.test.ts +++ b/src/auto-reply/command-control.test.ts @@ -159,6 +159,48 @@ describe("resolveCommandAuthorization", () => { expect(otherAuth.isAuthorizedSender).toBe(false); }); + it("rejects wildcard channel senders when the plugin enforces owner-only commands", () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "discord", + plugin: { + ...createOutboundTestPlugin({ + id: "discord", + outbound: { deliveryMode: "direct" }, + }), + commands: { enforceOwnerForCommands: true }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + resolveAllowFrom: () => ["*"], + formatAllowFrom, + }, + }, + source: "test", + }, + ]), + ); + const cfg = { + channels: { discord: { allowFrom: ["*"] } }, + } as OpenClawConfig; + + const auth = resolveCommandAuthorization({ + ctx: { + Provider: "discord", + Surface: "discord", + ChatType: "direct", + From: "discord:123", + SenderId: "123", + } as MsgContext, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(false); + expect(auth.isAuthorizedSender).toBe(false); + }); + it("uses explicit owner allowlist when allowFrom is empty", () => { const cfg = { commands: { ownerAllowFrom: ["whatsapp:+15551234567"] },