openclaw - ✅(Solved) Fix [Bug]: /allowlist --store bypasses channel configWrites policy [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72360Fetched 2026-04-27 05:30:58
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

Root Cause

This finding crosses the configWrites authorization boundary, which is OpenClaw's documented control for restricting which channels may make persistent configuration changes. The configWrites=false setting is an explicit operator-configured trust boundary: it prevents a given channel from initiating writes that modify access control state. The /allowlist --store path bypasses this boundary by skipping resolveConfigWriteDeniedText entirely when parsed.target === "store", allowing a sender on a write-restricted channel to directly mutate the pairing allowlist store — a security-relevant credential file under ~/.openclaw/credentials/. This is not out of scope: it is not a multi-tenant isolation claim, not a prompt-injection-only path, and not a trusted-operator-initiated action; the attacker is an already-command-authorized sender, not the gateway operator, and the bypass defeats a control the operator deliberately configured to restrict writes from that channel.

Impact

An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.

Fix Action

Fix / Workaround

This finding crosses the configWrites authorization boundary, which is OpenClaw's documented control for restricting which channels may make persistent configuration changes. The configWrites=false setting is an explicit operator-configured trust boundary: it prevents a given channel from initiating writes that modify access control state. The /allowlist --store path bypasses this boundary by skipping resolveConfigWriteDeniedText entirely when parsed.target === "store", allowing a sender on a write-restricted channel to directly mutate the pairing allowlist store — a security-relevant credential file under ~/.openclaw/credentials/. This is not out of scope: it is not a multi-tenant isolation claim, not a prompt-injection-only path, and not a trusted-operator-initiated action; the attacker is an already-command-authorized sender, not the gateway operator, and the bypass defeats a control the operator deliberately configured to restrict writes from that channel.

Impact

An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.

The store mutation is security-relevant. readChannelAllowFromStore (src/pairing/pairing-store.ts:542-563) returns persisted entries, which mergeDmAllowFromSources and resolveEffectiveAllowFromLists (src/security/dm-policy-shared.ts:44-50) merge into the effective DM allowlist. resolveSenderCommandAuthorization (src/plugin-sdk/command-auth.ts:72-105) then uses these merged lists for access decisions, meaning a new sender enrolled via the bypassed store mutation will be authorized for future DM commands.

PR fix notes

PR #72361: fix: /allowlist --store bypasses channel configWrites policy

Description (problem / solution / changelog)

Summary

  • Problem: An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.
  • Why it matters: Configure a pairing-capable channel (e.g.
  • What changed: Configure a pairing-capable channel (e.g.
  • What did NOT change (scope boundary): No unrelated defaults, migrations, or compatibility behavior were intentionally changed.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #72360
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: Configure a pairing-capable channel (e.g.
  • Missing detection / guardrail: No narrower detection note was recorded in the issue bundle.
  • Contributing context (if known): See the linked issue analysis and changed files below.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this: Validation command pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test
  • Target test or file: Not explicitly recorded in the issue bundle.
  • Scenario the test should lock in: An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.
  • Why this is the smallest reliable guardrail: It validates the failing behavior on the touched path without expanding scope.
  • Existing test that already covers this (if any): Unknown.
  • If no new test is added, why not: The staged bundle did not record a narrower regression target.

User-visible / Behavior Changes

  • None beyond resolving the linked issue's broken behavior.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): Yes
  • New/changed network calls? (Yes/No): No
  • Command/tool execution surface changed? (Yes/No): Yes
  • Data access scope changed? (Yes/No): Yes
  • If any Yes, explain risk + mitigation: Configure a pairing-capable channel (e.g.. Mitigation: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported passed.

Repro + Verification

Environment

  • OS: N/A
  • Runtime/container: N/A
  • Model/provider: opencode/glm-5.1
  • Integration/channel (if any): N/A
  • Relevant config (redacted): AI-assisted=yes

Steps

  1. Reproduce the linked issue using the recorded issue bundle.
  2. Apply the fix from this branch.
  3. Run pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test.

Expected

  • An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.
  • Validation passes for the touched behavior.

Actual

  • Validation status: passed

Evidence

  • Validation evidence: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported passed.
  • CVSS v3.1: 7.6 (High)
  • CVSS v4.0: 7.2 (High)
  • Changed files:
  • src/auto-reply/reply/commands-allowlist.test.ts (+70/-0)
  • src/auto-reply/reply/commands-allowlist.ts (+16/-0)

Human Verification (required)

  • Verified scenarios: An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.
  • Edge cases checked: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test completed with status passed.
  • What you did not verify: Interactive/manual scenarios not captured in the staged issue bundle.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No): Yes
  • Config/env changes? (Yes/No): No
  • Migration needed? (Yes/No): No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Regression in the touched behavior while closing the linked issue.
    • Mitigation: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported passed and the changed-file scope stayed limited to the recorded diff.

Changed files

  • src/auto-reply/reply/commands-allowlist.test.ts (modified, +70/-0)
  • src/auto-reply/reply/commands-allowlist.ts (modified, +16/-0)

Code Example

// Line 495-496: shouldUpdateConfig is false when --store is passed
const shouldUpdateConfig = parsed.target !== "store";
const shouldTouchStore = parsed.target !== "config" && listPairingChannels().includes(channelId);

// Lines 498-670: configWrites gate only runs when shouldUpdateConfig is true
if (shouldUpdateConfig) {
  // ...
  const deniedText = resolveConfigWriteDeniedText({
    cfg: params.cfg,
    channel: params.command.channel,
    channelId,
    accountId: params.ctx.AccountId,
    gatewayClientScopes: params.ctx.GatewayClientScopes,
    target: editSpec.writeTarget,
  });
  if (deniedText) {
    return { shouldContinue: false, reply: { text: deniedText } };
  }
  // ... config file mutation branch ...
}

// Lines 672-684: --store path reaches here with no configWrites check
if (!shouldTouchStore) {
  return { shouldContinue: false, reply: { text: "⚠️ This channel does not support allowlist storage." } };
}
await updatePairingStoreAllowlist({
  action: parsed.action,
  channelId,
  accountId,
  entry: parsed.entry,
});
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score7.6 / 10.07.2 / 10.0
SeverityHighHigh
VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:LCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

This finding crosses the configWrites authorization boundary, which is OpenClaw's documented control for restricting which channels may make persistent configuration changes. The configWrites=false setting is an explicit operator-configured trust boundary: it prevents a given channel from initiating writes that modify access control state. The /allowlist --store path bypasses this boundary by skipping resolveConfigWriteDeniedText entirely when parsed.target === "store", allowing a sender on a write-restricted channel to directly mutate the pairing allowlist store — a security-relevant credential file under ~/.openclaw/credentials/. This is not out of scope: it is not a multi-tenant isolation claim, not a prompt-injection-only path, and not a trusted-operator-initiated action; the attacker is an already-command-authorized sender, not the gateway operator, and the bypass defeats a control the operator deliberately configured to restrict writes from that channel.

Impact

An authorized sender on a channel where configWrites=false can invoke /allowlist add|remove --store <id> to add or remove entries from the persistent pairing allowlist store, defeating the operator's intent to block channel-initiated access-control writes. Because pairing-store entries are merged into the effective DM allowFrom list at authorization time, this bypass grants or revokes DM access for future senders without the operator's consent.

Affected Component

File: src/auto-reply/reply/commands-allowlist.ts:495-496,498-550,672-684

// Line 495-496: shouldUpdateConfig is false when --store is passed
const shouldUpdateConfig = parsed.target !== "store";
const shouldTouchStore = parsed.target !== "config" && listPairingChannels().includes(channelId);

// Lines 498-670: configWrites gate only runs when shouldUpdateConfig is true
if (shouldUpdateConfig) {
  // ...
  const deniedText = resolveConfigWriteDeniedText({
    cfg: params.cfg,
    channel: params.command.channel,
    channelId,
    accountId: params.ctx.AccountId,
    gatewayClientScopes: params.ctx.GatewayClientScopes,
    target: editSpec.writeTarget,
  });
  if (deniedText) {
    return { shouldContinue: false, reply: { text: deniedText } };
  }
  // ... config file mutation branch ...
}

// Lines 672-684: --store path reaches here with no configWrites check
if (!shouldTouchStore) {
  return { shouldContinue: false, reply: { text: "⚠️ This channel does not support allowlist storage." } };
}
await updatePairingStoreAllowlist({
  action: parsed.action,
  channelId,
  accountId,
  entry: parsed.entry,
});

Technical Reproduction

  1. Configure a pairing-capable channel (e.g. Telegram or WhatsApp) with commands.text=true, commands.config=true, channels.<provider>.dmPolicy="pairing", and channels.<provider>.configWrites=false.
  2. Ensure at least one sender is already command-authorized on that channel (via existing allowlist or pairing approval).
  3. Send /allowlist add dm --store <target-id> from that authorized sender.
  4. The handler sets shouldUpdateConfig = false because parsed.target === "store", bypasses the entire if (shouldUpdateConfig) block including the resolveConfigWriteDeniedText call, and falls through to updatePairingStoreAllowlist(...) at line 679, returning ✅ DM allowlist added: pairing store.
  5. Confirm the entry persists in ~/.openclaw/credentials/<channel>-allowFrom.json (or the account-scoped variant).
  6. Send a DM from the newly enrolled <target-id>. readChannelAllowFromStore(...) returns the entry, mergeDmAllowFromSources merges it into effectiveAllowFrom (src/security/dm-policy-shared.ts:44-50), and the sender is treated as authorized for future DM commands.

Demonstrated Impact

The root cause is in src/auto-reply/reply/commands-allowlist.ts: the configWrites authorization gate is gated inside the if (shouldUpdateConfig) branch (line 498), which is only entered when the user targets the config file. When the user passes --store, shouldUpdateConfig is false and the entire gate — including resolveConfigWriteDeniedText — is never evaluated. The code unconditionally reaches updatePairingStoreAllowlist at lines 679-684.

The store mutation is security-relevant. readChannelAllowFromStore (src/pairing/pairing-store.ts:542-563) returns persisted entries, which mergeDmAllowFromSources and resolveEffectiveAllowFromLists (src/security/dm-policy-shared.ts:44-50) merge into the effective DM allowlist. resolveSenderCommandAuthorization (src/plugin-sdk/command-auth.ts:72-105) then uses these merged lists for access decisions, meaning a new sender enrolled via the bypassed store mutation will be authorized for future DM commands.

The docs at docs/tools/slash-commands.md:127-128 explicitly state that /allowlist add|remove "honors channel configWrites," but the --store path violates that contract. An operator who set configWrites=false to prevent a shared or automated channel from making persistent access-control changes gets no protection when the attacker uses the --store flag.

Environment

Verified against openclaw/openclaw release v2026.4.24 (commit e672b61417af5c45b0431df6d9109a1f4b618ef5, published 2026-04-25T18:15:17Z).

Preconditions:

  • A pairing-capable channel (Telegram, WhatsApp, iMessage, Discord, or any channel returned by listPairingChannels())
  • commands.config=true on that channel
  • channels.<provider>.configWrites=false
  • At least one already command-authorized sender on that channel

Remediation Advice

Apply the same configWrites authorization check to store-targeted /allowlist mutations as to config-targeted ones by calling resolveConfigWriteDeniedText before reaching updatePairingStoreAllowlist, regardless of the parsed.target value. If pairing-store edits are intentionally meant to bypass configWrites, document a distinct permission key for that surface and update the slash-commands docs to reflect the separate boundary rather than advertising a single unified configWrites gate.

<!-- submission-marker:AA-lly-allowlist-store-bypasses-configwrites -->

extent analysis

TL;DR

The configWrites authorization check should be applied to store-targeted /allowlist mutations to prevent bypassing the configWrites=false setting.

Guidance

  • The issue arises from the shouldUpdateConfig check in src/auto-reply/reply/commands-allowlist.ts, which skips the configWrites gate when parsed.target === "store".
  • To fix this, call resolveConfigWriteDeniedText before reaching updatePairingStoreAllowlist, regardless of the parsed.target value.
  • Consider documenting a distinct permission key for pairing-store edits if they are intentionally meant to bypass configWrites.
  • Update the slash-commands docs to reflect the separate boundary or unified configWrites gate.

Example

if (parsed.target === "store") {
  const deniedText = resolveConfigWriteDeniedText({
    cfg: params.cfg,
    channel: params.command.channel,
    channelId,
    accountId: params.ctx.AccountId,
    gatewayClientScopes: params.ctx.GatewayClientScopes,
    target: editSpec.writeTarget,
  });
  if (deniedText) {
    return { shouldContinue: false, reply: { text: deniedText } };
  }
}
await updatePairingStoreAllowlist({
  action: parsed.action,
  channelId,
  accountId,
  entry: parsed.entry,
});

Notes

The provided code snippet assumes that the resolveConfigWriteDeniedText function is correctly implemented and returns a denied text if the configWrites check fails.

Recommendation

Apply the workaround by calling resolveConfigWriteDeniedText before reaching updatePairingStoreAllowlist to ensure the configWrites authorization check is applied to store-targeted /allowlist mutations.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: /allowlist --store bypasses channel configWrites policy [1 pull requests, 1 participants]