Fixed in OpenClaw 2026.3.24, the current shipping release.
Summary
The shared /allowlist command persists channel authorization config through writeConfigFile(...) but does not re-validate gateway client scopes for internal gateway callers. Because chat.send is intentionally reachable to operator.write callers and still creates a generic command-authorized internal context, an authenticated write-scoped gateway client can indirectly mutate channel allowFrom and groupAllowFrom policy that direct config.patch correctly reserves to operator.admin.
This is not just a generic code smell. The current code already shows the intended boundary by adding sink-side internal admin checks to shared /config and /plugins writes, but /allowlist was left behind.
Details
The gateway's documented scope split is clear:
chat.send is a write-scoped action.
- direct config mutation is an admin-scoped action.
The vulnerable path is:
- A gateway client authenticates with
operator.write.
- The client calls
chat.send, which is intentionally allowed for that scope.
chat.send builds an internal message context with CommandAuthorized: true and carries GatewayClientScopes into the reply pipeline.
resolveCommandAuthorization(...) converts that internal message into isAuthorizedSender=true in the common case where no stricter commands.allowFrom override is configured.
/allowlist add|remove accepts that generic command authorization and proceeds into its config-backed edit path.
- The handler clones the parsed config, calls
plugin.allowlist.applyConfigEdit(...), validates the result, and persists it with writeConfigFile(validated.config).
- No sink-side check requires
operator.admin before the persistent write occurs.
That creates a direct control-plane mismatch:
config.patch rejects the same caller with missing scope: operator.admin.
/allowlist add dm ... or /allowlist add group ... reached through chat.send can still rewrite channel authorization state.
Impact
- A gateway client intentionally limited to
operator.write can persist first-party channel authorization policy.
- The caller can widen DM or group allowlists for channels using the shared
/allowlist plumbing.
- This weakens the repo's documented control-plane privilege split between ordinary write actions and admin-only persistent authorization mutation.
Remediation
1) Add the Missing Sink-Side Internal Admin Check to /allowlist
Mirror the existing hardened pattern from /config and /plugins.
Before any config-backed /allowlist add|remove write, require:
operator.admin for internal gateway channels
This should happen before plugin.allowlist.applyConfigEdit(...) and before writeConfigFile(...).
2) Keep Pairing-Store and Config-Write Policy Checks, but Do Not Treat Them as Scope Enforcement
configWrites policy and pairing-store behavior are useful secondary controls, but they do not replace the missing privilege check between operator.write and operator.admin.
References
Summary
The shared
/allowlistcommand persists channel authorization config throughwriteConfigFile(...)but does not re-validate gateway client scopes for internal gateway callers. Becausechat.sendis intentionally reachable tooperator.writecallers and still creates a generic command-authorized internal context, an authenticated write-scoped gateway client can indirectly mutate channelallowFromandgroupAllowFrompolicy that directconfig.patchcorrectly reserves tooperator.admin.This is not just a generic code smell. The current code already shows the intended boundary by adding sink-side internal admin checks to shared
/configand/pluginswrites, but/allowlistwas left behind.Details
The gateway's documented scope split is clear:
chat.sendis a write-scoped action.The vulnerable path is:
operator.write.chat.send, which is intentionally allowed for that scope.chat.sendbuilds an internal message context withCommandAuthorized: trueand carriesGatewayClientScopesinto the reply pipeline.resolveCommandAuthorization(...)converts that internal message intoisAuthorizedSender=truein the common case where no strictercommands.allowFromoverride is configured./allowlist add|removeaccepts that generic command authorization and proceeds into its config-backed edit path.plugin.allowlist.applyConfigEdit(...), validates the result, and persists it withwriteConfigFile(validated.config).operator.adminbefore the persistent write occurs.That creates a direct control-plane mismatch:
config.patchrejects the same caller withmissing scope: operator.admin./allowlist add dm ...or/allowlist add group ...reached throughchat.sendcan still rewrite channel authorization state.Impact
operator.writecan persist first-party channel authorization policy./allowlistplumbing.Remediation
1) Add the Missing Sink-Side Internal Admin Check to
/allowlistMirror the existing hardened pattern from
/configand/plugins.Before any config-backed
/allowlist add|removewrite, require:operator.adminfor internal gateway channelsThis should happen before
plugin.allowlist.applyConfigEdit(...)and beforewriteConfigFile(...).2) Keep Pairing-Store and Config-Write Policy Checks, but Do Not Treat Them as Scope Enforcement
configWritespolicy and pairing-store behavior are useful secondary controls, but they do not replace the missing privilege check betweenoperator.writeandoperator.admin.References