VYPR
Medium severity4.2NVD Advisory· Published Apr 9, 2026· Updated Apr 16, 2026

CVE-2026-35617

CVE-2026-35617

Description

OpenClaw before 2026.3.25 contains an authorization bypass vulnerability in Google Chat group policy enforcement that relies on mutable space display names. Attackers can rebind group policies by changing or colliding space display names to gain unauthorized access to protected resources.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.25

Patches

1
11ea1f67863d

Google Chat: require stable group ids (#55131)

https://github.com/openclaw/openclawJacob TomlinsonMar 26, 2026via ghsa
2 files changed · +175 8
  • extensions/googlechat/src/monitor-access.test.ts+125 0 modified
    @@ -230,4 +230,129 @@ describe("googlechat inbound access policy", () => {
     
         expect(logVerbose).toHaveBeenCalledWith("googlechat: drop control command from users/alice");
       });
    +
    +  it("does not match group policy by mutable space displayName when the stable id differs", async () => {
    +    primeCommonDefaults();
    +    createChannelPairingController.mockReturnValue({
    +      readAllowFromStore: vi.fn(async () => []),
    +      issueChallenge: vi.fn(),
    +    });
    +    resolveDmGroupAccessWithLists.mockReturnValue({
    +      decision: "allow",
    +      effectiveAllowFrom: [],
    +      effectiveGroupAllowFrom: ["users/alice"],
    +    });
    +    resolveMentionGatingWithBypass.mockReturnValue({
    +      shouldSkip: false,
    +      effectiveWasMentioned: true,
    +    });
    +    const logVerbose = vi.fn();
    +
    +    const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js");
    +
    +    await expect(
    +      applyGoogleChatInboundAccessPolicy({
    +        account: {
    +          accountId: "default",
    +          config: {
    +            groups: {
    +              "Finance Ops": {
    +                users: ["users/alice"],
    +                requireMention: true,
    +                systemPrompt: "finance-only prompt",
    +              },
    +            },
    +          },
    +        } as never,
    +        config: {
    +          channels: { googlechat: {} },
    +          commands: { useAccessGroups: true },
    +        } as never,
    +        core: createCore() as never,
    +        space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
    +        message: {
    +          annotations: [
    +            {
    +              type: "USER_MENTION",
    +              userMention: { user: { name: "users/app" } },
    +            },
    +          ],
    +        } as never,
    +        isGroup: true,
    +        senderId: "users/alice",
    +        senderName: "Alice",
    +        senderEmail: "alice@example.com",
    +        rawBody: "show quarter close status",
    +        logVerbose,
    +      }),
    +    ).resolves.toEqual({ ok: false });
    +
    +    expect(logVerbose).toHaveBeenCalledWith(
    +      "Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: Finance Ops",
    +    );
    +    expect(logVerbose).toHaveBeenCalledWith(
    +      "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
    +    );
    +  });
    +
    +  it("fails closed instead of falling back to wildcard when a deprecated room key matches", async () => {
    +    primeCommonDefaults();
    +    resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
    +      groupPolicy: "open",
    +      providerMissingFallbackApplied: false,
    +    });
    +    createChannelPairingController.mockReturnValue({
    +      readAllowFromStore: vi.fn(async () => []),
    +      issueChallenge: vi.fn(),
    +    });
    +    resolveDmGroupAccessWithLists.mockReturnValue({
    +      decision: "allow",
    +      effectiveAllowFrom: [],
    +      effectiveGroupAllowFrom: ["users/alice"],
    +    });
    +    resolveMentionGatingWithBypass.mockReturnValue({
    +      shouldSkip: false,
    +      effectiveWasMentioned: true,
    +    });
    +    const logVerbose = vi.fn();
    +
    +    const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js");
    +
    +    await expect(
    +      applyGoogleChatInboundAccessPolicy({
    +        account: {
    +          accountId: "default",
    +          config: {
    +            groupPolicy: "open",
    +            groups: {
    +              "*": {
    +                users: ["users/alice"],
    +              },
    +              "Finance Ops": {
    +                allow: false,
    +                users: ["users/bob"],
    +              },
    +            },
    +          },
    +        } as never,
    +        config: {
    +          channels: { googlechat: {} },
    +          commands: { useAccessGroups: true },
    +        } as never,
    +        core: createCore() as never,
    +        space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
    +        message: { annotations: [] } as never,
    +        isGroup: true,
    +        senderId: "users/alice",
    +        senderName: "Alice",
    +        senderEmail: "alice@example.com",
    +        rawBody: "show quarter close status",
    +        logVerbose,
    +      }),
    +    ).resolves.toEqual({ ok: false });
    +
    +    expect(logVerbose).toHaveBeenCalledWith(
    +      "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
    +    );
    +  });
     });
    
  • extensions/googlechat/src/monitor-access.ts+50 8 modified
    @@ -78,16 +78,29 @@ function resolveGroupConfig(params: {
       const entries = groups ?? {};
       const keys = Object.keys(entries);
       if (keys.length === 0) {
    -    return { entry: undefined, allowlistConfigured: false };
    -  }
    -  const normalizedName = groupName?.trim().toLowerCase();
    -  const candidates = [groupId, groupName ?? "", normalizedName ?? ""].filter(Boolean);
    -  let entry = candidates.map((candidate) => entries[candidate]).find(Boolean);
    -  if (!entry && normalizedName) {
    -    entry = entries[normalizedName];
    +    return { entry: undefined, allowlistConfigured: false, deprecatedNameMatch: false };
       }
    +  const entry = entries[groupId];
    +  const normalizedGroupName = groupName?.trim().toLowerCase() ?? "";
    +  const deprecatedNameMatch =
    +    !entry &&
    +    Boolean(
    +      groupName &&
    +      keys.some((key) => {
    +        const trimmed = key.trim();
    +        if (!trimmed || trimmed === "*" || /^spaces\//i.test(trimmed)) {
    +          return false;
    +        }
    +        return trimmed === groupName || trimmed.toLowerCase() === normalizedGroupName;
    +      }),
    +    );
       const fallback = entries["*"];
    -  return { entry: entry ?? fallback, allowlistConfigured: true, fallback };
    +  return {
    +    entry: deprecatedNameMatch ? undefined : (entry ?? fallback),
    +    allowlistConfigured: true,
    +    fallback,
    +    deprecatedNameMatch,
    +  };
     }
     
     function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: string | null) {
    @@ -108,6 +121,7 @@ function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: strin
     }
     
     const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
    +const warnedMutableGroupKeys = new Set<string>();
     
     function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void, entries: string[]) {
       const deprecated = entries.map((v) => String(v).trim()).filter((v) => /^users\/.+@.+/i.test(v));
    @@ -127,6 +141,29 @@ function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void,
       );
     }
     
    +function warnMutableGroupKeysConfigured(
    +  logVerbose: (message: string) => void,
    +  groups?: Record<string, GoogleChatGroupEntry>,
    +) {
    +  const mutableKeys = Object.keys(groups ?? {})
    +    .map((key) => key.trim())
    +    .filter((key) => key && key !== "*" && !/^spaces\//i.test(key));
    +  if (mutableKeys.length === 0) {
    +    return;
    +  }
    +  const warningKey = mutableKeys
    +    .map((key) => key.toLowerCase())
    +    .sort()
    +    .join(",");
    +  if (warnedMutableGroupKeys.has(warningKey)) {
    +    return;
    +  }
    +  warnedMutableGroupKeys.add(warningKey);
    +  logVerbose(
    +    `Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: ${mutableKeys.join(", ")}`,
    +  );
    +}
    +
     export async function applyGoogleChatInboundAccessPolicy(params: {
       account: ResolvedGoogleChatAccount;
       config: OpenClawConfig;
    @@ -185,6 +222,7 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
         blockedLabel: GROUP_POLICY_BLOCKED_LABEL.space,
         log: logVerbose,
       });
    +  warnMutableGroupKeysConfigured(logVerbose, account.config.groups ?? undefined);
       const groupConfigResolved = resolveGroupConfig({
         groupId: spaceId,
         groupName: space.displayName ?? null,
    @@ -195,6 +233,10 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
       let effectiveWasMentioned: boolean | undefined;
     
       if (isGroup) {
    +    if (groupConfigResolved.deprecatedNameMatch) {
    +      logVerbose(`drop group message (deprecated mutable group key matched, space=${spaceId})`);
    +      return { ok: false };
    +    }
         const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
         const routeAccess = evaluateGroupRouteAccessForPolicy({
           groupPolicy,
    

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

5

News mentions

0

No linked articles in our index yet.