Skip to content

[Security] Elevated allowFrom sender-scope bypass via recipient and mutable sender metadata in openclaw-cn #564

Description

@YLChen-007

Advisory Details

Title: Elevated allowFrom sender-scope bypass via recipient and mutable sender metadata in openclaw-cn

Description:

Summary

openclaw-cn contains an authorization flaw in the /elevated directive path. A chat sender who has already passed the broader command gate can still enable elevated mode without matching the intended sender-scoped allowlist, because the elevated authorization helper also treats the recipient field (To) and mutable sender metadata (SenderName, SenderUsername, SenderTag) as valid matches for unprefixed tools.elevated.allowFrom.<provider> entries. This weakens the intended privilege boundary between ordinary command-capable chat users and the smaller set of users who should be allowed to activate elevated execution behavior.

Details

The vulnerable logic is in src/auto-reply/reply/reply-elevated.ts, inside isApprovedElevatedSender(). The function resolves the configured allowFrom entries for the active provider and then builds a token set from several MsgContext fields. The bug is that this token set mixes stable sender identity with unrelated or weak-trust fields:

addToken(params.ctx.SenderName);
addToken(params.ctx.SenderUsername);
addToken(params.ctx.SenderTag);
addToken(params.ctx.SenderE164);
addToken(params.ctx.From);
addToken(stripSenderPrefix(params.ctx.From));
addToken(params.ctx.To);
addToken(stripSenderPrefix(params.ctx.To));

Because unprefixed allowlist entries are compared against that entire set, a sender can be incorrectly approved when:

  1. The configured allowlist value matches the recipient/self address in ctx.To, even though the actual sender is different.
  2. The configured allowlist value matches mutable profile metadata such as SenderName, even though the stable sender identity is not allowlisted.

The vulnerable authorization decision is then consumed in src/auto-reply/reply/get-reply-directives.ts:

const elevated = resolveElevatedPermissions({
  cfg,
  agentId,
  ctx,
  provider: messageProviderKey,
});
...
if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) {
  return { kind: "reply", reply: { text: formatElevatedUnavailableMessage(...) } };
}

If elevatedAllowed is incorrectly computed as true, the /elevated on directive is accepted and the session's elevatedLevel is updated instead of being rejected.

This was verified on the real code path used by getReplyFromConfig() and resolveReplyDirectives(). The current evidence is unit-level rather than full WhatsApp device E2E, but it exercises the live configuration loader, route resolution, directive parsing, elevated authorization helper, and session persistence without source modification or mocks of the vulnerable component.

PoC

Prerequisites

  • A checkout of the affected openclaw-cn repository.
  • python3 and bun installed locally.
  • A message context that has already reached CommandAuthorized: true.
  • A configuration that uses tools.elevated.allowFrom.whatsapp to restrict elevated access more narrowly than the general command-authorized surface.

Reproduction Steps

  1. Download the TypeScript driver from: driver.ts
  2. Download the verification harness from: verification_test.py
  3. Download the control harness from: control-normal-behavior.py
  4. From the repository root, run the verification harness:
    python3 llm-enhance/cve-finding/similar/improper-privilege-management/Advisory-GHSA-f6h3-846h-2r8w-elevated-allowfrom-sender-scope-bypass-exp/verification_test.py
  5. Observe that both experiment cases succeed:
    • recipient_vuln: the sender is +15550001000, but tools.elevated.allowFrom.whatsapp=["+15550002000"] and To="+15550002000", causing elevated mode to be enabled.
    • mutable_vuln: the sender's stable identity is unchanged, but SenderName="owner-display-name" and tools.elevated.allowFrom.whatsapp=["owner-display-name"], causing elevated mode to be enabled.
  6. Run the control harness:
    python3 llm-enhance/cve-finding/similar/improper-privilege-management/Advisory-GHSA-f6h3-846h-2r8w-elevated-allowfrom-sender-scope-bypass-exp/control-normal-behavior.py
  7. Confirm the protected baseline:
    • when To does not match the allowlist, /elevated on is denied;
    • when SenderName does not match the allowlist, /elevated on is denied.

Log of Evidence

Verification run:

Verification Mode: Unit-Test
Data flow: inbound DM config/state -> checkInboundAccessControl -> resolveWhatsAppCommandAuthorized -> getReplyFromConfig -> resolveReplyDirectives -> resolveElevatedPermissions -> session store elevatedLevel
[DEFECT CONFIRMED WITH LIMITATIONS] recipient-token and mutable-sender elevated allowFrom bypasses reproduced

Observed experiment behavior:

  • recipient_vuln returned Elevated mode set to ask (approvals may still apply). and persisted elevatedLevel: "on".
  • mutable_vuln returned Elevated mode set to ask (approvals may still apply). and persisted elevatedLevel: "on".

Control run:

Control Mode: Unit-Test
[CONTROL PASS] protected baseline preserved for both control cases

Observed control behavior:

  • recipient_control returned elevated is not available right now ... and left elevatedLevel: null.
  • mutable_control returned elevated is not available right now ... and left elevatedLevel: null.

Impact

This is an improper privilege management issue in the chat control plane. It impacts deployments that rely on tools.elevated.allowFrom.<provider> to create a narrower elevated-execution boundary than the general command boundary. A command-authorized but non-elevated sender can activate elevated mode by matching recipient routing or mutable sender metadata rather than a stable sender identity. In deployments where elevated mode unlocks stronger execution behavior, approvals, or host-side actions, this allows a lower-privileged chat actor to cross into a more sensitive execution state than intended.

Affected products

  • Ecosystem: npm
  • Package name: openclaw-cn
  • Affected versions: <= 0.2.1
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L

Weaknesses

  • CWE: CWE-269: Improper Privilege Management

Occurrences

Permalink Description
function isApprovedElevatedSender(params: {
provider: string;
ctx: MsgContext;
allowFrom?: AgentElevatedAllowFromConfig;
fallbackAllowFrom?: Array<string | number>;
}): boolean {
const rawAllow = resolveElevatedAllowList(
params.allowFrom,
params.provider,
params.fallbackAllowFrom,
);
if (!rawAllow || rawAllow.length === 0) return false;
const allowTokens = rawAllow.map((entry) => String(entry).trim()).filter(Boolean);
if (allowTokens.length === 0) return false;
if (allowTokens.some((entry) => entry === "*")) return true;
const tokens = new Set<string>();
const addToken = (value?: string) => {
if (!value) return;
const trimmed = value.trim();
if (!trimmed) return;
tokens.add(trimmed);
const normalized = normalizeAllowToken(trimmed);
if (normalized) tokens.add(normalized);
const slugged = slugAllowToken(trimmed);
if (slugged) tokens.add(slugged);
};
addToken(params.ctx.SenderName);
addToken(params.ctx.SenderUsername);
addToken(params.ctx.SenderTag);
addToken(params.ctx.SenderE164);
addToken(params.ctx.From);
addToken(stripSenderPrefix(params.ctx.From));
addToken(params.ctx.To);
addToken(stripSenderPrefix(params.ctx.To));
for (const rawEntry of allowTokens) {
const entry = rawEntry.trim();
if (!entry) continue;
const stripped = stripSenderPrefix(entry);
if (tokens.has(entry) || tokens.has(stripped)) return true;
const normalized = normalizeAllowToken(stripped);
if (normalized && tokens.has(normalized)) return true;
const slugged = slugAllowToken(stripped);
isApprovedElevatedSender() builds the approval token set. The vulnerable logic adds ctx.To, SenderName, SenderUsername, and SenderTag into the same default-matching pool as stable sender identifiers, enabling recipient-token and mutable-metadata bypasses.
const messageProviderKey =
sessionCtx.Provider?.trim().toLowerCase() ?? ctx.Provider?.trim().toLowerCase() ?? "";
const elevated = resolveElevatedPermissions({
cfg,
agentId,
ctx,
provider: messageProviderKey,
});
const elevatedEnabled = elevated.enabled;
const elevatedAllowed = elevated.allowed;
const elevatedFailures = elevated.failures;
if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) {
typing.cleanup();
const runtimeSandboxed = resolveSandboxRuntimeStatus({
cfg,
sessionKey: ctx.SessionKey,
}).sandboxed;
return {
kind: "reply",
reply: {
text: formatElevatedUnavailableMessage({
runtimeSandboxed,
failures: elevatedFailures,
sessionKey: ctx.SessionKey,
}),
},
};
}
resolveReplyDirectives() consumes the result of resolveElevatedPermissions(). When the flawed elevated authorization returns true, the /elevated directive is accepted instead of returning the denial response.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions