From c8424bf29a921e25663b29f308640b3d91a49432 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 15:31:26 +0100 Subject: [PATCH] fix(googlechat): deprecate users/ allowlists (#16243) --- CHANGELOG.md | 1 + docs/channels/googlechat.md | 3 +- extensions/googlechat/src/monitor.test.ts | 16 +++--- extensions/googlechat/src/monitor.ts | 61 +++++++++++++++++------ extensions/googlechat/src/onboarding.ts | 2 +- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a4d6497f9..3e0892a6f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Security: fix Chutes manual OAuth login state validation (thanks @aether-ai-agent). (#16058) - macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins. +- Security/Google Chat: deprecate `users/` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc. ## 2026.2.14 diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index 39192ecae2f..818a8288f5d 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -153,7 +153,8 @@ Configure your tunnel's ingress rules to only route the webhook path: Use these identifiers for delivery and allowlists: -- Direct messages: `users/` or `users/` (email addresses are accepted). +- Direct messages: `users/` (recommended) or raw email `name@example.com` (mutable principal). +- Deprecated: `users/` is treated as a user id, not an email allowlist. - Spaces: `spaces/`. ## Config highlights diff --git a/extensions/googlechat/src/monitor.test.ts b/extensions/googlechat/src/monitor.test.ts index 5223ba9c9fd..6eec88abbe4 100644 --- a/extensions/googlechat/src/monitor.test.ts +++ b/extensions/googlechat/src/monitor.test.ts @@ -2,21 +2,21 @@ import { describe, expect, it } from "vitest"; import { isSenderAllowed } from "./monitor.js"; describe("isSenderAllowed", () => { - it("matches allowlist entries with users/", () => { - expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(true); - }); - it("matches allowlist entries with raw email", () => { expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(true); }); + it("does not treat users/ entries as email allowlist (deprecated form)", () => { + expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe( + false, + ); + }); + it("still matches user id entries", () => { expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true); }); - it("rejects non-matching emails", () => { - expect(isSenderAllowed("users/123", "jane@example.com", ["users/other@example.com"])).toBe( - false, - ); + it("rejects non-matching raw email entries", () => { + expect(isSenderAllowed("users/123", "jane@example.com", ["other@example.com"])).toBe(false); }); }); diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 4ca340e845c..9482cd0bce0 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -61,6 +61,31 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, } } +const warnedDeprecatedUsersEmailAllowFrom = new Set(); +function warnDeprecatedUsersEmailEntries( + core: GoogleChatCoreRuntime, + runtime: GoogleChatRuntimeEnv, + entries: string[], +) { + const deprecated = entries.map((v) => String(v).trim()).filter((v) => /^users\/.+@.+/i.test(v)); + if (deprecated.length === 0) { + return; + } + const key = deprecated + .map((v) => v.toLowerCase()) + .sort() + .join(","); + if (warnedDeprecatedUsersEmailAllowFrom.has(key)) { + return; + } + warnedDeprecatedUsersEmailAllowFrom.add(key); + logVerbose( + core, + runtime, + `Deprecated allowFrom entry detected: "users/" is no longer treated as an email allowlist. Use raw email (alice@example.com) or immutable user id (users/). entries=${deprecated.join(", ")}`, + ); +} + function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { @@ -285,6 +310,11 @@ function normalizeUserId(raw?: string | null): string { return trimmed.replace(/^users\//i, "").toLowerCase(); } +function isEmailLike(value: string): boolean { + // Keep this intentionally loose; allowlists are user-provided config. + return value.includes("@"); +} + export function isSenderAllowed( senderId: string, senderEmail: string | undefined, @@ -300,22 +330,19 @@ export function isSenderAllowed( if (!normalized) { return false; } - if (normalized === normalizedSenderId) { - return true; + + // Accept `googlechat:` but treat `users/...` as an *ID* only (deprecated `users/`). + const withoutPrefix = normalized.replace(/^(googlechat|google-chat|gchat):/i, ""); + if (withoutPrefix.startsWith("users/")) { + return normalizeUserId(withoutPrefix) === normalizedSenderId; } - if (normalizedEmail && normalized === normalizedEmail) { - return true; + + // Raw email allowlist entries remain supported for usability. + if (normalizedEmail && isEmailLike(withoutPrefix)) { + return withoutPrefix === normalizedEmail; } - if (normalizedEmail && normalized.replace(/^users\//i, "") === normalizedEmail) { - return true; - } - if (normalized.replace(/^users\//i, "") === normalizedSenderId) { - return true; - } - if (normalized.replace(/^(googlechat|google-chat|gchat):/i, "") === normalizedSenderId) { - return true; - } - return false; + + return withoutPrefix.replace(/^users\//i, "") === normalizedSenderId; }); } @@ -473,6 +500,11 @@ async function processMessageWithPipeline(params: { } if (groupUsers.length > 0) { + warnDeprecatedUsersEmailEntries( + core, + runtime, + groupUsers.map((v) => String(v)), + ); const ok = isSenderAllowed( senderId, senderEmail, @@ -493,6 +525,7 @@ async function processMessageWithPipeline(params: { ? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => []) : []; const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; + warnDeprecatedUsersEmailEntries(core, runtime, effectiveAllowFrom); const commandAllowFrom = isGroup ? groupUsers.map((v) => String(v)) : effectiveAllowFrom; const useAccessGroups = config.commands?.useAccessGroups !== false; const senderAllowedForCommands = isSenderAllowed(senderId, senderEmail, commandAllowFrom); diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts index 263f1029bcd..41d04218735 100644 --- a/extensions/googlechat/src/onboarding.ts +++ b/extensions/googlechat/src/onboarding.ts @@ -55,7 +55,7 @@ async function promptAllowFrom(params: { }): Promise { const current = params.cfg.channels?.["googlechat"]?.dm?.allowFrom ?? []; const entry = await params.prompter.text({ - message: "Google Chat allowFrom (user id or email)", + message: "Google Chat allowFrom (users/ or raw email; avoid users/)", placeholder: "users/123456789, name@example.com", initialValue: current[0] ? String(current[0]) : undefined, validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),