VYPR
Moderate severityNVD Advisory· Published Mar 19, 2026· Updated Mar 25, 2026

OpenClaw 2026.2.22 < 2026.2.24 - Authorization Bypass in Synology Chat Plugin via Empty allowedUserIds

CVE-2026-31998

Description

OpenClaw versions 2026.2.22 and 2026.2.23 contain an authorization bypass vulnerability in the synology-chat channel plugin where dmPolicy set to allowlist with empty allowedUserIds fails open. Attackers with Synology sender access can bypass authorization checks and trigger unauthorized agent dispatch and downstream tool actions.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
>= 2026.2.22, < 2026.2.242026.2.24

Affected products

1

Patches

2
7655c0cb3a47

docs(changelog): add synology-chat allowlist fail-closed note

https://github.com/openclaw/openclawPeter SteinbergerFeb 24, 2026via ghsa
1 file changed · +1 0
  • CHANGELOG.md+1 0 modified
    @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
     - Config/Plugins: treat stale removed `google-antigravity-auth` plugin references as compatibility warnings (not hard validation errors) across `plugins.entries`, `plugins.allow`, `plugins.deny`, and `plugins.slots.memory`, so startup no longer fails after antigravity removal. (#25538, #25862) Thanks @chilu18.
     - Security/Message actions: enforce local media root checks for `sendAttachment` and `setGroupIcon` when `sandboxRoot` is unset, preventing attachment hydration from reading arbitrary host files via local absolute paths. This ships in the next npm release. Thanks @GCXWLP for reporting.
     - Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
    +- Security/Synology Chat: enforce fail-closed allowlist behavior for DM ingress so `dmPolicy: "allowlist"` with empty `allowedUserIds` rejects all senders instead of allowing unauthorized dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Exec approvals: bind `system.run` command display/approval text to full argv when shell-wrapper inline payloads carry positional argv values, and reject payload-only `rawCommand` mismatches for those wrapper-carrier forms, preventing hidden command execution under misleading approval text. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Exec: limit default safe-bin trusted directories to immutable system paths (`/bin`, `/usr/bin`) and require explicit opt-in (`tools.exec.safeBinTrustedDirs`) for package-manager/user bin paths (for example Homebrew), preventing writable-dir binary shadowing from auto-satisfying safe-bin allowlist checks. This ships in the next npm release. Thanks @tdjackey for reporting.
    
0ee30361b8f6

fix(synology-chat): fail closed empty allowlist

https://github.com/openclaw/openclawPeter SteinbergerFeb 24, 2026via ghsa
7 files changed · +63 13
  • docs/channels/synology-chat.md+1 0 modified
    @@ -72,6 +72,7 @@ Config values override env vars.
     
     - `dmPolicy: "allowlist"` is the recommended default.
     - `allowedUserIds` accepts a list (or comma-separated string) of Synology user IDs.
    +- In `allowlist` mode, an empty `allowedUserIds` list blocks all senders (use `dmPolicy: "open"` for allow-all).
     - `dmPolicy: "open"` allows any sender.
     - `dmPolicy: "disabled"` blocks DMs.
     - Pairing approvals work with:
    
  • extensions/synology-chat/src/channel.test.ts+19 0 modified
    @@ -183,6 +183,25 @@ describe("createSynologyChatPlugin", () => {
           expect(warnings.some((w: string) => w.includes("open"))).toBe(true);
         });
     
    +    it("warns when dmPolicy is allowlist and allowedUserIds is empty", () => {
    +      const plugin = createSynologyChatPlugin();
    +      const account = {
    +        accountId: "default",
    +        enabled: true,
    +        token: "t",
    +        incomingUrl: "https://nas/incoming",
    +        nasHost: "h",
    +        webhookPath: "/w",
    +        dmPolicy: "allowlist" as const,
    +        allowedUserIds: [],
    +        rateLimitPerMinute: 30,
    +        botName: "Bot",
    +        allowInsecureSsl: false,
    +      };
    +      const warnings = plugin.security.collectWarnings({ account });
    +      expect(warnings.some((w: string) => w.includes("empty allowedUserIds"))).toBe(true);
    +    });
    +
         it("returns no warnings for fully configured account", () => {
           const plugin = createSynologyChatPlugin();
           const account = {
    
  • extensions/synology-chat/src/channel.ts+5 0 modified
    @@ -141,6 +141,11 @@ export function createSynologyChatPlugin() {
                 '- Synology Chat: dmPolicy="open" allows any user to message the bot. Consider "allowlist" for production use.',
               );
             }
    +        if (account.dmPolicy === "allowlist" && account.allowedUserIds.length === 0) {
    +          warnings.push(
    +            '- Synology Chat: dmPolicy="allowlist" with empty allowedUserIds blocks all senders. Add users or set dmPolicy="open".',
    +          );
    +        }
             return warnings;
           },
         },
    
  • extensions/synology-chat/src/security.test.ts+2 2 modified
    @@ -24,8 +24,8 @@ describe("validateToken", () => {
     });
     
     describe("checkUserAllowed", () => {
    -  it("allows any user when allowlist is empty", () => {
    -    expect(checkUserAllowed("user1", [])).toBe(true);
    +  it("rejects user when allowlist is empty", () => {
    +    expect(checkUserAllowed("user1", [])).toBe(false);
       });
     
       it("allows user in the allowlist", () => {
    
  • extensions/synology-chat/src/security.ts+1 2 modified
    @@ -22,10 +22,9 @@ export function validateToken(received: string, expected: string): boolean {
     
     /**
      * Check if a user ID is in the allowed list.
    - * Empty allowlist = allow all users.
    + * Allowlist mode must be explicit; empty lists should not match any user.
      */
     export function checkUserAllowed(userId: string, allowedUserIds: string[]): boolean {
    -  if (allowedUserIds.length === 0) return true;
       return allowedUserIds.includes(userId);
     }
     
    
  • extensions/synology-chat/src/webhook-handler.test.ts+20 0 modified
    @@ -156,6 +156,26 @@ describe("createWebhookHandler", () => {
         });
       });
     
    +  it("returns 403 when allowlist policy is set with empty allowedUserIds", async () => {
    +    const deliver = vi.fn();
    +    const handler = createWebhookHandler({
    +      account: makeAccount({
    +        dmPolicy: "allowlist",
    +        allowedUserIds: [],
    +      }),
    +      deliver,
    +      log,
    +    });
    +
    +    const req = makeReq("POST", validBody);
    +    const res = makeRes();
    +    await handler(req, res);
    +
    +    expect(res._status).toBe(403);
    +    expect(res._body).toContain("Allowlist is empty");
    +    expect(deliver).not.toHaveBeenCalled();
    +  });
    +
       it("returns 403 when DMs are disabled", async () => {
         await expectForbiddenByPolicy({
           account: { dmPolicy: "disabled" },
    
  • extensions/synology-chat/src/webhook-handler.ts+15 9 modified
    @@ -138,20 +138,26 @@ export function createWebhookHandler(deps: WebhookHandlerDeps) {
         }
     
         // User allowlist check
    -    if (
    -      account.dmPolicy === "allowlist" &&
    -      !checkUserAllowed(payload.user_id, account.allowedUserIds)
    -    ) {
    -      log?.warn(`Unauthorized user: ${payload.user_id}`);
    -      respond(res, 403, { error: "User not authorized" });
    -      return;
    -    }
    -
         if (account.dmPolicy === "disabled") {
           respond(res, 403, { error: "DMs are disabled" });
           return;
         }
     
    +    if (account.dmPolicy === "allowlist") {
    +      if (account.allowedUserIds.length === 0) {
    +        log?.warn("Synology Chat allowlist is empty while dmPolicy=allowlist; rejecting message");
    +        respond(res, 403, {
    +          error: "Allowlist is empty. Configure allowedUserIds or use dmPolicy=open.",
    +        });
    +        return;
    +      }
    +      if (!checkUserAllowed(payload.user_id, account.allowedUserIds)) {
    +        log?.warn(`Unauthorized user: ${payload.user_id}`);
    +        respond(res, 403, { error: "User not authorized" });
    +        return;
    +      }
    +    }
    +
         // Rate limit
         if (!rateLimiter.check(payload.user_id)) {
           log?.warn(`Rate limit exceeded for user: ${payload.user_id}`);
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.