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
- Download the main probe from: owner_only_tool_policy_gap_probe.ts
- Download the verification wrapper from: verification_test.py
- Download the control wrapper from: control-direct-write-scope-denial.py
- 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
- Run the verification wrapper:
python3 verification_test.py
- 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.
- Run the control wrapper:
python3 control-direct-write-scope-denial.py
- 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. |
Advisory Details
Title: Non-owner agent tool dispatch exposes admin-only
cronandgatewayactions in openclaw-cnDescription:
Summary
An improper privilege management issue in
openclaw-cnallows a lower-privileged, authenticated Gateway operator context to trigger an admin-only cron action through the embedded agent tool-dispatch path. A directcron.runrequest from anoperator.writeclient is correctly rejected, but the same non-owner context can still receive the owner-onlycronandgatewaytools from the livecreateOpenClawCodingTools(...)path. When thecrontool 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.tsdefinesapplyOwnerOnlyToolPolicy(...), and the fallback owner-only name set explicitly includescron,gateway, andwhatsapp_login:However, the live
createOpenClawCodingTools(...)implementation assembles the complete tool list, includingcreateOpenClawTools(...), without ever applying that owner-only filter:This is reachable from the embedded runner's real execution path.
src/agents/pi-embedded-runner/run/attempt.tsforwardssenderIsOwnerintocreateOpenClawCodingTools(...):The downstream sink is privileged. The
crontool usescallGatewayTool("cron.run", ...), which resolves intosrc/gateway/call.ts. That helper falls back to the locally configured Gateway token and always reconnects as an operator with admin-class scopes:This matters because
src/gateway/server-methods.tscorrectly enforcescron.runas admin-only for direct callers: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
openclaw-cnat an affected version, verified against GitHub release tagv0.2.0.-exp/folder.Reproduction Steps
cd llm-enhance/cve-finding/similar/improper-privilege-management/Advisory-GHSA-jr6x-2q95-fh2g-owner-only-tool-policy-gap-exppython3 verification_test.pycron.runis denied for the write-scoped client, then shows the non-owner tool list still containscronandgateway, and finally recordscron_entries_after_tool: 1.python3 control-direct-write-scope-denial.pyoperator.writeclient cannot create any run entry when callingcron.rundirectly, confirming the baseline scope enforcement is working and only the agent tool path bypasses it.Log of Evidence
Verification run:
Control run:
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 leavesgatewayexposed in the non-owner tool list. In practical terms, that breaks the trustworthiness of the product's scope separation: code that expectsoperator.writeclients to be unable to perform administrative control-plane actions can be bypassed through the agent tool path.Affected products
Severity
Weaknesses
Occurrences
cron,gateway, andwhatsapp_loginas owner-only tools and providesapplyOwnerOnlyToolPolicy(...), establishing the intended security rule.createOpenClawCodingTools(...)path assembles the final tool list, includingcreateOpenClawTools(...), without applying the owner-only tool policy before returning tools to the agent runtime.senderIsOwnerinto the live tool builder, proving the missing policy application is on a reachable runtime path rather than dead code.operator.admin,operator.approvals, andoperator.pairingscopes, causing tool execution to run with elevated privileges.cron.runas admin-only, demonstrating that the bypass is specific to the non-owner agent tool path rather than intended behavior.