Skip to content

[Security] Non-owner agent tool dispatch exposes admin-only cron and gateway actions in openclaw-cn #599

Description

@YLChen-007

Advisory Details

Title: Non-owner agent tool dispatch exposes admin-only cron and gateway actions in openclaw-cn

Description:

Summary

An improper privilege management issue in openclaw-cn allows a lower-privileged, authenticated Gateway operator context to trigger an admin-only cron action through the embedded agent tool-dispatch path. A direct cron.run request from an operator.write client is correctly rejected, but the same non-owner context can still receive the owner-only cron and gateway tools from the live createOpenClawCodingTools(...) path. When the cron tool executes, it calls back into the Gateway using host-level admin credentials, bypassing the original caller's scope boundary.

Details

The bug is in the mismatch between the owner-only policy definition and the live tool assembly path.

src/agents/tool-policy.ts defines applyOwnerOnlyToolPolicy(...), and the fallback owner-only name set explicitly includes cron, gateway, and whatsapp_login:

const OWNER_ONLY_TOOL_NAME_FALLBACKS = new Set<string>(["whatsapp_login", "cron", "gateway"]);

export function applyOwnerOnlyToolPolicy(tools: AnyAgentTool[], senderIsOwner: boolean) {
  const withGuard = tools.map((tool) => {
    if (!isOwnerOnlyTool(tool)) {
      return tool;
    }
    return wrapOwnerOnlyToolExecution(tool, senderIsOwner);
  });
  if (senderIsOwner) {
    return withGuard;
  }
  return withGuard.filter((tool) => !isOwnerOnlyTool(tool));
}

However, the live createOpenClawCodingTools(...) implementation assembles the complete tool list, including createOpenClawTools(...), without ever applying that owner-only filter:

  const tools: AnyAgentTool[] = [
    ...base,
    ...listChannelAgentTools({ cfg: options?.config }),
    ...createOpenClawTools({
      agentSessionKey: options?.sessionKey,
      agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
      ...
    }),
  ];

This is reachable from the embedded runner's real execution path. src/agents/pi-embedded-runner/run/attempt.ts forwards senderIsOwner into createOpenClawCodingTools(...):

      : createOpenClawCodingTools({
          ...
          senderIsOwner: params.senderIsOwner,
          sessionKey: params.sessionKey ?? params.sessionId,
          ...
        });

The downstream sink is privileged. The cron tool uses callGatewayTool("cron.run", ...), which resolves into src/gateway/call.ts. That helper falls back to the locally configured Gateway token and always reconnects as an operator with admin-class scopes:

  const authToken = config.gateway?.auth?.token;
  ...
  const token =
    explicitAuth.token ||
    (!urlOverride
      ? isRemoteMode
        ? ...
        : process.env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
          (typeof authToken === "string" && authToken.trim().length > 0
            ? authToken.trim()
            : undefined)
      : undefined);
  ...
    const client = new GatewayClient({
      ...
      role: "operator",
      scopes: ["operator.admin", "operator.approvals", "operator.pairing"],

This matters because src/gateway/server-methods.ts correctly enforces cron.run as admin-only for direct callers:

  if (
    ...
    method === "cron.add" ||
    method === "cron.update" ||
    method === "cron.remove" ||
    method === "cron.run" ||
    ...
  ) {
    return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
  }

In other words, the product already defines a real scope boundary here. The vulnerable behavior is that the non-owner agent tool path bypasses that boundary by exposing owner-only tools and then silently rebinding the action onto host credentials.

PoC

Prerequisites

  • A local checkout of openclaw-cn at an affected version, verified against GitHub release tag v0.2.0.
  • Node.js / Bun environment capable of starting the real Gateway locally.
  • Python 3 available to run the wrapper scripts.
  • No external services are required; the PoC creates its own isolated Gateway state, token auth configuration, and paired devices under the -exp/ folder.

Reproduction Steps

  1. Download the main probe from: owner_only_tool_policy_gap_probe.ts
  2. Download the verification wrapper from: verification_test.py
  3. Download the control wrapper from: control-direct-write-scope-denial.py
  4. Change into the exploit directory:
    cd llm-enhance/cve-finding/similar/improper-privilege-management/Advisory-GHSA-jr6x-2q95-fh2g-owner-only-tool-policy-gap-exp
  5. Run the verification wrapper:
    python3 verification_test.py
  6. Observe that the script first proves direct cron.run is denied for the write-scoped client, then shows the non-owner tool list still contains cron and gateway, and finally records cron_entries_after_tool: 1.
  7. Run the control wrapper:
    python3 control-direct-write-scope-denial.py
  8. Observe that the same operator.write client cannot create any run entry when calling cron.run directly, confirming the baseline scope enforcement is working and only the agent tool path bypasses it.

Log of Evidence

Verification run:

$ bun owner_only_tool_policy_gap_probe.ts verify verification_result.json
[ws] ⇄ res ✗ cron.run 0ms errorCode=INVALID_REQUEST errorMessage=missing scope: operator.admin
{
  "classification": "DEFECT-CONFIRMED-WITH-LIMITATIONS",
  "baseline_denied": true,
  "cron_entries_after_direct": 0,
  "non_owner_tool_names": [
    "cron",
    "gateway"
  ],
  "non_owner_has_cron": true,
  "non_owner_has_gateway": true,
  "cron_entries_after_tool": 1,
  "cron_tool_result": {
    "details": {
      "ok": true,
      "ran": true
    }
  }
}

Control run:

$ bun owner_only_tool_policy_gap_probe.ts control control_result.json
[ws] ⇄ res ✗ cron.run 0ms errorCode=INVALID_REQUEST errorMessage=missing scope: operator.admin
{
  "classification": "PROTECTED-BASELINE",
  "baseline_denied": true,
  "cron_entries_after_direct": 0
}

Impact

This is an authorization bypass / improper privilege management issue affecting the product's Gateway control plane and embedded agent runtime. The immediate impact is that a lower-privileged, authenticated operator context can trigger an action that the Gateway explicitly reserves for admin scope. The verified sink is cron.run, but the same missing owner-only filtering also leaves gateway exposed in the non-owner tool list. In practical terms, that breaks the trustworthiness of the product's scope separation: code that expects operator.write clients to be unable to perform administrative control-plane actions can be bypassed through the agent tool path.

Affected products

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

Severity

  • Severity: High
  • 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
https://github.com/jiulingyun/openclaw-cn/blob/1b9f16468d9841871cb15103693c3923424c9842/src/agents/tool-policy.ts#L61-L105 The repository defines cron, gateway, and whatsapp_login as owner-only tools and provides applyOwnerOnlyToolPolicy(...), establishing the intended security rule.
https://github.com/jiulingyun/openclaw-cn/blob/1b9f16468d9841871cb15103693c3923424c9842/src/agents/pi-tools.ts#L320-L350 The live createOpenClawCodingTools(...) path assembles the final tool list, including createOpenClawTools(...), without applying the owner-only tool policy before returning tools to the agent runtime.
https://github.com/jiulingyun/openclaw-cn/blob/1b9f16468d9841871cb15103693c3923424c9842/src/agents/pi-embedded-runner/run/attempt.ts#L275-L305 The embedded runner forwards senderIsOwner into the live tool builder, proving the missing policy application is on a reachable runtime path rather than dead code.
https://github.com/jiulingyun/openclaw-cn/blob/1b9f16468d9841871cb15103693c3923424c9842/src/gateway/call.ts#L176-L255 The Gateway call helper falls back to the host Gateway token and reconnects with operator.admin, operator.approvals, and operator.pairing scopes, causing tool execution to run with elevated privileges.
https://github.com/jiulingyun/openclaw-cn/blob/1b9f16468d9841871cb15103693c3923424c9842/src/gateway/server-methods.ts#L118-L146 The direct Gateway request path correctly enforces cron.run as admin-only, demonstrating that the bypass is specific to the non-owner agent tool path rather than intended behavior.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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