Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default {
"settings.weixinRemoteApprovalRequiresOwner":
"Exactly one authenticated WeChat account whose owner is in the explicit sender allowlist is required.",
"settings.weixinRemoteApprovalOnceOnly":
"Replies approve only the currently listed request(s) once. They never create durable approvals, folder grants, or future approval. Secret-bearing or unclassified free-text command fields stay local-only.",
"Remote prompts are limited to exact predefined read-only Windows queries with no opaque operands. Free-form, unknown, or secret-bearing requests stay local-only, and raw command fields are not sent to WeChat. Replies approve only the currently listed request(s) once and never create durable approvals, folder grants, or future approval.",
"settings.weixinRemoteApprovalEnable": "Enable and bind locally",
"settings.weixinRemoteApprovalDisable": "Disable",
"settings.weixinRemoteApprovalRevoke": "Revoke owner binding",
Expand Down
2 changes: 1 addition & 1 deletion desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export default {
"settings.weixinRemoteApprovalRequiresOwner":
"必须且只能存在一个已登录微信账户,且其所有者必须位于显式发送者白名单中。",
"settings.weixinRemoteApprovalOnceOnly":
"回复仅一次性批准当前列出的请求;绝不会创建永久审批、文件夹授权或未来请求授权。包含秘密或无法分类的自由文本命令字段仅允许本地审批。",
"远程提示仅支持不含不透明操作数的精确预定义 Windows 只读查询。自由文本、未知或含密请求仅能在本机审批,原始命令字段不会发送到微信。回复仅一次性批准当前列出的请求;绝不会创建永久审批、文件夹授权或未来请求授权。",
"settings.weixinRemoteApprovalEnable": "在本机确认启用并绑定",
"settings.weixinRemoteApprovalDisable": "禁用",
"settings.weixinRemoteApprovalRevoke": "撤销所有者绑定",
Expand Down
4 changes: 2 additions & 2 deletions desktop/renderer/src/views/SettingsView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ describe("SettingsView", () => {
expect(card.text()).toContain("WeChat remote approval (Experimental)");
expect(card.text()).toContain("owner@im.wechat");
expect(card.text()).toContain("allowlisted openclaw-weixin plugin and Gateway");
expect(card.text()).toContain("Free-form, unknown, or secret-bearing requests stay local-only");
expect(card.text()).toContain("never create durable approvals");
expect(card.text()).toContain("unclassified free-text command fields stay local-only");
await wrapper.find('[data-testid="weixin-remote-approval-enable"]').trigger("click");
await flushPromises();
expect(setRemoteApprovalEnabled).toHaveBeenCalledWith(true);
Expand All @@ -376,8 +376,8 @@ describe("SettingsView", () => {
const card = wrapper.find('[data-testid="weixin-remote-approval"]');
expect(card.text()).toContain("微信远程审批(实验性)");
expect(card.text()).toContain("白名单 openclaw-weixin 插件和 Gateway");
expect(card.text()).toContain("自由文本、未知或含密请求仅能在本机审批");
expect(card.text()).toContain("绝不会创建永久审批");
expect(card.text()).toContain("无法分类的自由文本命令字段仅允许本地审批");
});

it("keeps technical MXC details collapsed on a fresh Settings view", async () => {
Expand Down
161 changes: 102 additions & 59 deletions desktop/src/windows-node-mxc-weixin-remote-approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";

import type { WindowsNodeMxcGatewayApproval } from "./windows-node-mxc";
import {
classifyWeixinRemoteApprovalPresentation,
migrateWeixinRemoteApprovalConfiguration,
normalizeWeixinRemoteApprovalOwner,
renderWeixinRemoteApprovalPrompt,
Expand All @@ -13,10 +14,10 @@ import {

const approval: WindowsNodeMxcGatewayApproval = {
id: "approval-1",
command: "whoami.exe /all",
executable: "whoami.exe",
arguments: ["/all"],
executablePath: "C:\\Windows\\System32\\whoami.exe",
command: "hostname.exe",
executable: "hostname.exe",
arguments: [],
executablePath: "C:\\Windows\\System32\\hostname.exe",
executableSha256: "a".repeat(64),
canonicalCwd: "isolated-scratch:v1",
agent: "main",
Expand Down Expand Up @@ -133,6 +134,61 @@ describe("WeChat remote approval broker", () => {
expect(() => normalizeWeixinRemoteApprovalOwner("bot", "display name")).toThrow();
});

it.each([
["hostname.exe", "C:\\Windows\\System32\\hostname.exe", [], "hostname.exe"],
["whoami.exe", "C:\\Windows\\System32\\whoami.exe", [], "whoami.exe"],
["whoami.exe", "C:\\Windows\\System32\\whoami.exe", ["/user"], "whoami.exe /user"],
["whoami.exe", "C:\\Windows\\System32\\whoami.exe", ["/groups"], "whoami.exe /groups"],
["whoami.exe", "C:\\Windows\\System32\\whoami.exe", ["/priv"], "whoami.exe /priv"],
["ipconfig.exe", "C:\\Windows\\System32\\ipconfig.exe", [], "ipconfig.exe"],
["ipconfig.exe", "C:\\Windows\\System32\\ipconfig.exe", ["/all"], "ipconfig.exe /all"],
["systeminfo.exe", "C:\\Windows\\System32\\systeminfo.exe", [], "systeminfo.exe"],
] as const)(
"allows only the exact predefined tuple %s %j",
(executable, executablePath, argumentsValue, command) => {
const broker = configuredBroker();
const allowed = input();
allowed.requests = [
{
approval: {
...approval,
executable,
executablePath,
arguments: [...argumentsValue],
command,
},
},
];
expect(broker.createChallenge(allowed)).not.toBeNull();
},
);

it.each([
["unknown executable", { executable: "curl.exe" }],
["unknown executable path", { executablePath: "C:\\Tools\\hostname.exe" }],
["unknown argument", { arguments: ["/unknown"] }],
["command drift", { command: "hostname.exe " }],
["declaration", { declaredAccess: [{ access: "ro" as const, path: "C:\\safe" }] }],
["non-scratch CWD", { canonicalCwd: "C:\\Temp" }],
["mutable operand", { hasMutableFileOperand: true }],
[
"missing one-use decision",
{ allowedDecisions: ["deny"] as WindowsNodeMxcGatewayApproval["allowedDecisions"] },
],
])("keeps structurally unrecognized %s requests local-only", (_name, approvalOverride) => {
const broker = configuredBroker();
const blocked = input();
blocked.requests = [{ approval: { ...approval, ...approvalOverride } }];
expect(broker.createChallenge(blocked)).toBeNull();
});

it("keeps unknown effective containment modes local-only", () => {
const broker = configuredBroker();
const blocked = input();
blocked.security = { ...blocked.security, effectiveMode: "future-tier" };
expect(broker.createChallenge(blocked)).toBeNull();
});

it.each([
["1", "allow-once", ["approval-1"]],
["2", "deny", ["approval-1"]],
Expand Down Expand Up @@ -172,10 +228,10 @@ describe("WeChat remote approval broker", () => {
approval: {
...approval,
id: "approval-2",
command: "hostname.exe",
executable: "hostname.exe",
executablePath: "C:\\Windows\\System32\\hostname.exe",
arguments: [],
command: "whoami.exe /groups",
executable: "whoami.exe",
executablePath: "C:\\Windows\\System32\\whoami.exe",
arguments: ["/groups"],
},
},
];
Expand Down Expand Up @@ -324,42 +380,37 @@ describe("WeChat remote approval broker", () => {
).toMatchObject({ status: "rejected", reason: "stale" });
});

it("renders safely classified values without trimming, path rewriting, or number conversion", () => {
it("never sends raw authorization fields and keeps arbitrary free text local-only", () => {
const broker = configuredBroker();
const exact = input();
const exactPath = "C:\\Users\\other-user\\Documents\\Data-01.txt";
exact.requests = [
{
approval: {
...approval,
command: `whoami.exe ${exactPath} --mode -7.50`,
arguments: [exactPath, "--mode", "-7.50"],
},
},
];
const delivery = broker.createChallenge(exact)!;
expect(delivery.text).toContain(JSON.stringify(exactPath));
expect(delivery.text).toContain(JSON.stringify("-7.50"));
expect(delivery.text).not.toContain("%USERPROFILE%");
});
const delivery = broker.createChallenge(input())!;
expect(delivery.text).toContain("Windows host name");
for (const rawValue of [
approval.command,
approval.executable,
approval.executablePath,
approval.executableSha256,
approval.canonicalCwd,
approval.agent!,
approval.sessionKey,
]) {
expect(delivery.text).not.toContain(rawValue);
}

it.each([
'{"safe":1,"safe":2,"number":9007199254740993}',
"Cafe\u0301",
"任意文本",
])("keeps unclassified exact text local-only without transforming it: %s", (value) => {
const broker = configuredBroker();
const exact = input();
exact.requests = [
const arbitrary = input();
const duplicateJson = '{"safe":1,"safe":2,"number":9007199254740993}';
const decomposed = "Cafe\u0301";
arbitrary.requests = [
{
approval: {
...approval,
command: `whoami.exe "${value}"`,
arguments: [value],
command: `tool.exe "${decomposed}"`,
executable: "tool.exe",
executablePath: "C:\\Tools\\tool.exe",
arguments: ["C:\\Users\\other-user\\Documents\\data.txt", duplicateJson, decomposed],
},
},
];
expect(broker.createChallenge(exact)).toBeNull();
expect(broker.createChallenge(arbitrary)).toBeNull();
});

it.each([
Expand Down Expand Up @@ -388,21 +439,14 @@ describe("WeChat remote approval broker", () => {
});

it.each([
"--******",
"--passphrase=correct-horse-battery-staple",
"AccountKey=abcdefghijklmnopqrstuvwxyz0123456789+/=",
"sv=2026-01-01&sig=abcdefghijklmnopqrstuvwxyz0123456789",
"npm_abcdefghijklmnopqrstuvwxyz0123456789",
["xoxb", "123456789012", "123456789012", "abcdefghijklmnopqrstuvwxyz"].join("-"),
])("keeps known missed credential forms local-only: %s", (secretValue) => {
const broker = configuredBroker();
const unsafe = input();
unsafe.requests = [{ approval: { ...approval, arguments: [secretValue] } }];
expect(broker.createChallenge(unsafe)).toBeNull();
expect(JSON.stringify(broker.getAuditEvents())).not.toContain(secretValue);
});

it.each([
"opaque-positional-password-value",
"--passphrase=hunter2",
"AccountKey=azure-storage-secret",
"sv=2024-01-01&sig=azure-sas-secret",
"//registry.npmjs.org/:_authToken=npm-secret",
"xoxb-1234567890-1234567890-opaque",
"X-Auth: custom-credential",
"dGhpcy1pcy1hbi11bmtub3duLWNyZWRlbnRpYWw=",
"--token=inline-secret",
"--password adjacent-secret",
"Authorization: Bearer abcdef",
Expand All @@ -424,7 +468,6 @@ describe("WeChat remote approval broker", () => {
});

it.each([
"--opaque=value",
"hunter2",
"shortvalue",
"123456",
Expand All @@ -443,9 +486,7 @@ describe("WeChat remote approval broker", () => {
it("keeps an unknown switch value local-only even when both tokens look harmless", () => {
const broker = configuredBroker();
const unsafe = input();
unsafe.requests = [
{ approval: { ...approval, arguments: ["--opaque", "shortvalue"] } },
];
unsafe.requests = [{ approval: { ...approval, arguments: ["--opaque", "shortvalue"] } }];
expect(broker.createChallenge(unsafe)).toBeNull();
});

Expand Down Expand Up @@ -488,19 +529,21 @@ describe("WeChat remote approval broker", () => {
});

it("renders truthful English and Chinese one-use option copy", () => {
const presentation = classifyWeixinRemoteApprovalPresentation([{ approval }], input().security);
expect(presentation).not.toBeNull();
for (const locale of ["en-US", "zh-CN"] as const) {
const prompt = renderWeixinRemoteApprovalPrompt({
code: "23456789",
expiresAtUnixMs: 123_000,
requests: [{ approval }],
security: input().security,
presentation: presentation!,
locale,
});
expect(prompt).toContain(`1 23456789`);
expect(prompt).toContain(`2 23456789`);
expect(prompt).toContain(`3 23456789`);
expect(prompt).toContain(approval.executableSha256);
expect(prompt).toContain(JSON.stringify(approval.canonicalCwd));
expect(prompt).not.toContain(approval.executableSha256);
expect(prompt).not.toContain(approval.canonicalCwd);
expect(prompt).not.toContain(approval.sessionKey);
expect(prompt).toMatch(
locale === "zh-CN" ? /不会创建永久授权/ : /no durable or future approval/,
);
Expand Down
Loading
Loading