Skip to content

Commit 7ade355

Browse files
committed
fix: gate synology chat reply name matching
1 parent 55ad5d7 commit 7ade355

13 files changed

Lines changed: 206 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Docs: https://docs.openclaw.ai
8888
- Web tools/Exa: align the bundled Exa plugin with the current Exa API by supporting newer search types and richer `contents` options, while fixing the result-count cap to honor Exa's higher limit. Thanks @vincentkoc.
8989
- Plugins/Matrix: move bundled plugin `KeyedAsyncQueue` imports onto the stable `plugin-sdk/core` surface so Matrix Docker/runtime builds do not depend on the brittle keyed-async-queue subpath. Thanks @ecohash-co and @vincentkoc.
9090
- Nostr/security: enforce inbound DM policy before decrypt, route Nostr DMs through the standard reply pipeline, and add pre-crypto rate and size guards so unknown senders cannot bypass pairing or force unbounded crypto work. Thanks @kuranikaran.
91+
- Synology Chat/security: keep reply delivery bound to stable numeric `user_id` by default, and gate mutable username/nickname recipient lookup behind `dangerouslyAllowNameMatching` with new regression coverage.
9192
- Agents/default timeout: raise the shared default agent timeout from `600s` to `48h` so long-running ACP and agent sessions do not fail unless you configure a shorter limit.
9293
- Gateway/Linux: auto-detect nvm-managed Node TLS CA bundle needs before CLI startup and refresh installed services that are missing `NODE_EXTRA_CA_CERTS`. (#51146) Thanks @GodsBoy.
9394
- Android/pairing: resolve portless secure setup URLs to `443` while preserving direct cleartext gateway defaults and explicit `:80` manual endpoints in onboarding. (#43540) Thanks @fmercurio.

docs/channels/synology-chat.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Config values override env vars.
7979
- In `allowlist` mode, an empty `allowedUserIds` list is treated as misconfiguration and the webhook route will not start (use `dmPolicy: "open"` for allow-all).
8080
- `dmPolicy: "open"` allows any sender.
8181
- `dmPolicy: "disabled"` blocks DMs.
82+
- Reply recipient binding stays on stable numeric `user_id` by default. `channels.synology-chat.dangerouslyAllowNameMatching: true` is break-glass compatibility mode that re-enables mutable username/nickname lookup for reply delivery.
8283
- Pairing approvals work with:
8384
- `openclaw pairing list synology-chat`
8485
- `openclaw pairing approve synology-chat <CODE>`
@@ -132,3 +133,4 @@ on two different Synology accounts does not share transcript state.
132133
- Keep `allowInsecureSsl: false` unless you explicitly trust a self-signed local NAS cert.
133134
- Inbound webhook requests are token-verified and rate-limited per sender.
134135
- Prefer `dmPolicy: "allowlist"` for production.
136+
- Keep `dangerouslyAllowNameMatching` off unless you explicitly need legacy username-based reply delivery.

docs/gateway/security/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ schema:
313313
- `channels.googlechat.dangerouslyAllowNameMatching`
314314
- `channels.googlechat.accounts.<accountId>.dangerouslyAllowNameMatching`
315315
- `channels.msteams.dangerouslyAllowNameMatching`
316+
- `channels.synology-chat.dangerouslyAllowNameMatching` (extension channel)
317+
- `channels.synology-chat.accounts.<accountId>.dangerouslyAllowNameMatching` (extension channel)
316318
- `channels.zalouser.dangerouslyAllowNameMatching` (extension channel)
317319
- `channels.irc.dangerouslyAllowNameMatching` (extension channel)
318320
- `channels.irc.accounts.<accountId>.dangerouslyAllowNameMatching` (extension channel)

extensions/synology-chat/src/accounts.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe("resolveAccount", () => {
6666
expect(account.accountId).toBe("default");
6767
expect(account.enabled).toBe(true);
6868
expect(account.webhookPath).toBe("/webhook/synology");
69+
expect(account.dangerouslyAllowNameMatching).toBe(false);
6970
expect(account.dmPolicy).toBe("allowlist");
7071
expect(account.rateLimitPerMinute).toBe(30);
7172
expect(account.botName).toBe("OpenClaw");
@@ -100,15 +101,56 @@ describe("resolveAccount", () => {
100101
"synology-chat": {
101102
token: "base-tok",
102103
botName: "BaseName",
104+
dangerouslyAllowNameMatching: false,
103105
accounts: {
104-
work: { token: "work-tok", botName: "WorkBot" },
106+
work: {
107+
token: "work-tok",
108+
botName: "WorkBot",
109+
dangerouslyAllowNameMatching: true,
110+
},
105111
},
106112
},
107113
},
108114
};
109115
const account = resolveAccount(cfg, "work");
110116
expect(account.token).toBe("work-tok");
111117
expect(account.botName).toBe("WorkBot");
118+
expect(account.dangerouslyAllowNameMatching).toBe(true);
119+
});
120+
121+
it("inherits dangerous name matching from base config when not overridden", () => {
122+
const cfg = {
123+
channels: {
124+
"synology-chat": {
125+
dangerouslyAllowNameMatching: true,
126+
accounts: {
127+
work: { token: "work-tok" },
128+
},
129+
},
130+
},
131+
};
132+
133+
const account = resolveAccount(cfg, "work");
134+
expect(account.dangerouslyAllowNameMatching).toBe(true);
135+
});
136+
137+
it("allows a named account to disable inherited dangerous name matching", () => {
138+
const cfg = {
139+
channels: {
140+
"synology-chat": {
141+
dangerouslyAllowNameMatching: true,
142+
accounts: {
143+
work: {
144+
token: "work-tok",
145+
dangerouslyAllowNameMatching: false,
146+
},
147+
},
148+
},
149+
},
150+
};
151+
152+
const account = resolveAccount(cfg, "work");
153+
expect(account.dangerouslyAllowNameMatching).toBe(false);
112154
});
113155

114156
it("parses comma-separated allowedUserIds string", () => {

extensions/synology-chat/src/accounts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export function resolveAccount(
8989
incomingUrl: merged.incomingUrl ?? envIncomingUrl,
9090
nasHost: merged.nasHost ?? envNasHost,
9191
webhookPath: merged.webhookPath ?? "/webhook/synology",
92+
dangerouslyAllowNameMatching: merged.dangerouslyAllowNameMatching ?? false,
9293
dmPolicy: merged.dmPolicy ?? "allowlist",
9394
allowedUserIds: parseAllowedUserIds(merged.allowedUserIds ?? envAllowedUserIds),
9495
rateLimitPerMinute: merged.rateLimitPerMinute ?? envRateLimitValue,

extensions/synology-chat/src/channel.test-mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function makeSecurityAccount(overrides: Record<string, unknown> = {}) {
101101
incomingUrl: "https://nas/incoming",
102102
nasHost: "h",
103103
webhookPath: "/w",
104+
dangerouslyAllowNameMatching: false,
104105
dmPolicy: "allowlist" as const,
105106
allowedUserIds: [],
106107
rateLimitPerMinute: 30,

extensions/synology-chat/src/channel.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ describe("createSynologyChatPlugin", () => {
107107
incomingUrl: "u",
108108
nasHost: "h",
109109
webhookPath: "/w",
110+
dangerouslyAllowNameMatching: false,
110111
dmPolicy: "allowlist" as const,
111112
allowedUserIds: ["user1"],
112113
rateLimitPerMinute: 30,
@@ -171,6 +172,13 @@ describe("createSynologyChatPlugin", () => {
171172
expect(warnings.some((w: string) => w.includes("SSL"))).toBe(true);
172173
});
173174

175+
it("warns when dangerous name matching is enabled", () => {
176+
const plugin = createSynologyChatPlugin();
177+
const account = makeSecurityAccount({ dangerouslyAllowNameMatching: true });
178+
const warnings = plugin.security.collectWarnings({ account });
179+
expect(warnings.some((w: string) => w.includes("dangerouslyAllowNameMatching"))).toBe(true);
180+
});
181+
174182
it("warns when dmPolicy is open", () => {
175183
const plugin = createSynologyChatPlugin();
176184
const account = makeSecurityAccount({ dmPolicy: "open" });

extensions/synology-chat/src/channel.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ import { synologyChatSetupAdapter, synologyChatSetupWizard } from "./setup-surfa
2929
import type { ResolvedSynologyChatAccount } from "./types.js";
3030

3131
const CHANNEL_ID = "synology-chat";
32-
const SynologyChatConfigSchema = buildChannelConfigSchema(z.object({}).passthrough());
32+
const SynologyChatConfigSchema = buildChannelConfigSchema(
33+
z
34+
.object({
35+
dangerouslyAllowNameMatching: z.boolean().optional(),
36+
})
37+
.passthrough(),
38+
);
3339

3440
const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver<ResolvedSynologyChatAccount>({
3541
channelKey: CHANNEL_ID,
@@ -51,6 +57,7 @@ const synologyChatConfigAdapter = createHybridChannelConfigAdapter<ResolvedSynol
5157
"incomingUrl",
5258
"nasHost",
5359
"webhookPath",
60+
"dangerouslyAllowNameMatching",
5461
"dmPolicy",
5562
"allowedUserIds",
5663
"rateLimitPerMinute",
@@ -73,6 +80,9 @@ const collectSynologyChatSecurityWarnings =
7380
(account) =>
7481
account.allowInsecureSsl &&
7582
"- Synology Chat: SSL verification is disabled (allowInsecureSsl=true). Only use this for local NAS with self-signed certificates.",
83+
(account) =>
84+
account.dangerouslyAllowNameMatching &&
85+
"- Synology Chat: dangerouslyAllowNameMatching=true re-enables mutable username/nickname recipient matching for replies. Prefer stable numeric user IDs.",
7686
(account) =>
7787
account.dmPolicy === "open" &&
7888
'- Synology Chat: dmPolicy="open" allows any user to message the bot. Consider "allowlist" for production use.',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from "vitest";
2+
import { SynologyChatChannelConfigSchema } from "./config-schema.js";
3+
4+
describe("SynologyChatChannelConfigSchema", () => {
5+
it("exports dangerouslyAllowNameMatching in the JSON schema", () => {
6+
const properties = (SynologyChatChannelConfigSchema.schema.properties ?? {}) as Record<
7+
string,
8+
{ type?: string }
9+
>;
10+
11+
expect(properties.dangerouslyAllowNameMatching?.type).toBe("boolean");
12+
});
13+
14+
it("keeps the schema open for plugin-specific passthrough fields", () => {
15+
expect(SynologyChatChannelConfigSchema.schema.additionalProperties).toBe(true);
16+
});
17+
});
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
22
import { z } from "zod";
33

4-
export const SynologyChatChannelConfigSchema = buildChannelConfigSchema(z.object({}).passthrough());
4+
export const SynologyChatChannelConfigSchema = buildChannelConfigSchema(
5+
z
6+
.object({
7+
dangerouslyAllowNameMatching: z.boolean().optional(),
8+
})
9+
.passthrough(),
10+
);

0 commit comments

Comments
 (0)