VYPR
Medium severity5.9NVD Advisory· Published Apr 9, 2026· Updated Apr 17, 2026

CVE-2026-35622

CVE-2026-35622

Description

OpenClaw before 2026.3.22 contains an improper authentication verification vulnerability in Google Chat app-url webhook handling that accepts add-on principals outside intended deployment bindings. Attackers can bypass webhook authentication by providing non-deployment add-on principals to execute unauthorized actions through the Google Chat integration.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.222026.3.22

Affected products

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

Patches

2
630f1479c44f

build: prepare 2026.3.23-2

https://github.com/openclaw/openclawPeter SteinbergerMar 24, 2026via ghsa
3 files changed · +26 24
  • CHANGELOG.md+24 22 modified
    @@ -8,45 +8,47 @@ Docs: https://docs.openclaw.ai
     
     ### Changes
     
    -- ModelStudio/Qwen: add standard (pay-as-you-go) DashScope endpoints for China and global Qwen API keys alongside the existing Coding Plan endpoints, and relabel the provider group to `Qwen (Alibaba Cloud Model Studio)`. (#43878)
    -- UI/clarity: consolidate button primitives (`btn--icon`, `btn--ghost`, `btn--xs`), refine the Knot theme to a black-and-red palette with WCAG 2.1 AA contrast, add config icons for Diagnostics/CLI/Secrets/ACP/MCP sections, replace the roundness slider with discrete stops, and improve accessibility with aria-labels across usage filters. (#53272) Thanks @BunsDev.
    -- CSP/Control UI: compute SHA-256 hashes for inline `<script>` blocks in the served `index.html` and include them in the `script-src` CSP directive, keeping inline scripts blocked by default while allowing explicitly hashed bootstrap code. (#53307) Thanks @BunsDev.
    -
     ### Fixes
     
    -- Plugins/ClawHub: resolve plugin API compatibility against the active runtime version at install time, and add regression coverage for current `>=2026.3.22` ClawHub package checks so installs no longer fail behind the stale `1.2.0` constant. (#53157) Thanks @futhgar.
    -- CLI/channel auth: auto-select the single login-capable configured channel for `channels login`/`logout` instead of relying on the outbound message-channel resolver, so env-only or non-auth channels no longer cause false ambiguity errors. (#53254) Thanks @BunsDev.
    -- Control UI/auth: preserve operator scopes through the device-auth bypass path, ignore cached under-scoped operator tokens, and show a clear `operator.read` fallback message when a connection really lacks read scope, so operator sessions stop failing or blanking on read-backed pages. (#53110) Thanks @BunsDev.
    -- Plugins/uninstall: accept installed `clawhub:` specs and versionless ClawHub package names as uninstall targets, so `openclaw plugins uninstall clawhub:<package>` works again even when the recorded install was pinned to a version.
    -- Auth/OpenAI tokens: stop live gateway auth-profile writes from reverting freshly saved credentials back to stale in-memory values, and make `models auth paste-token` write to the resolved agent store, so Configure, Onboard, and token-paste flows stop snapping back to expired OpenAI tokens. Fixes #53207. Related to #45516.
    -- Agents/failover: classify generic `api_error` payloads as retryable only when they include transient failure signals, so MiniMax-style backend failures still trigger model fallback without misclassifying billing, auth, or format/context errors. (#49611) Thanks @ayushozha.
    -- Diagnostics/cache trace: strip credential fields from cache-trace JSONL output while preserving non-sensitive diagnostic fields and image redaction metadata.
    -- Docs/Feishu: replace `botName` with `name` in the channel config examples so the docs match the strict account schema for per-account display names. (#52753) Thanks @haroldfabla2-hue.
    -- Doctor/plugins: make `openclaw doctor --fix` remove stale `plugins.allow` and `plugins.entries` refs left behind after plugin removal. Thanks @sallyom
    -- Agents/replay: canonicalize malformed assistant transcript content before session-history sanitization so legacy or corrupted assistant turns stop crashing Pi replay and subagent recovery paths.
    -- ClawHub/skills: keep updating already-tracked legacy Unicode slugs after the ASCII-only slug hardening, so older installs do not get stuck behind `Invalid skill slug` errors during `openclaw skills update`. (#53206) Thanks @drobison00.
    -- Infra/exec trust: preserve shell-multiplexer wrapper binaries for policy checks without breaking approved-command reconstruction, so BusyBox/ToyBox allowlist and audit flows bind to the real wrapper while execution plans stay coherent. (#53134) Thanks @vincentkoc.
    -- LINE/runtime-api: pre-export overlapping runtime symbols before the `line-runtime` star export so jiti no longer throws `TypeError: Cannot redefine property` on startup. (#53221) Thanks @Drickon.
    -- CLI/cron: make `openclaw cron add|edit --at ... --tz <iana>` honor the requested local wall-clock time for offset-less one-shot datetimes, including DST boundaries, and keep `--tz` rejected for `--every`. (#53224) Thanks @RolfHegr.
    -- Commands/auth: stop slash-command authorization from crashing or dropping valid allowlists when channel `allowFrom` resolution hits unresolved SecretRef-backed accounts, and fail closed only for the affected provider inference path. (#52791) Thanks @Lukavyi.
    -
     ## 2026.3.23
     
     ### Breaking
     
     ### Changes
     
    +- ModelStudio/Qwen: add standard (pay-as-you-go) DashScope endpoints for China and global Qwen API keys alongside the existing Coding Plan endpoints, and relabel the provider group to `Qwen (Alibaba Cloud Model Studio)`. (#43878)
    +- UI/clarity: consolidate button primitives (`btn--icon`, `btn--ghost`, `btn--xs`), refine the Knot theme to a black-and-red palette with WCAG 2.1 AA contrast, add config icons for Diagnostics/CLI/Secrets/ACP/MCP sections, replace the roundness slider with discrete stops, and improve accessibility with aria-labels across usage filters. (#53272) Thanks @BunsDev.
    +- CSP/Control UI: compute SHA-256 hashes for inline `<script>` blocks in the served `index.html` and include them in the `script-src` CSP directive, keeping inline scripts blocked by default while allowing explicitly hashed bootstrap code. (#53307) Thanks @BunsDev.
    +
     ### Fixes
     
    +- Plugins/bundled runtimes: ship bundled plugin runtime sidecars like WhatsApp `light-runtime-api.js`, Matrix `runtime-api.js`, and other plugin runtime entry files in the npm package again, so global installs stop failing on missing bundled plugin runtime surfaces.
    +- CLI/channel auth: auto-select the single configured login-capable channel for `channels login`/`logout`, harden channel ids against prototype-chain and control-character abuse, and fall back cleanly to catalog-backed channel installs, so channel auth works again for single-channel setups and on-demand channel installs. (#53254) Thanks @BunsDev.
    +- Auth/OpenAI tokens: stop live gateway auth-profile writes from reverting freshly saved credentials back to stale in-memory values, and make `models auth paste-token` write to the resolved agent store, so Configure, Onboard, and token-paste flows stop snapping back to expired OpenAI tokens. Fixes #53207. Related to #45516.
    +- Control UI/auth: preserve operator scopes through the device-auth bypass path, ignore cached under-scoped operator tokens, and show a clear `operator.read` fallback message when a connection really lacks read scope, so operator sessions stop failing or blanking on read-backed pages. (#53110) Thanks @BunsDev.
    +- Plugins/ClawHub: resolve plugin API compatibility against the active runtime version at install time, and add regression coverage for current `>=2026.3.22` ClawHub package checks so installs no longer fail behind the stale `1.2.0` constant. (#53157) Thanks @futhgar.
    +- Plugins/uninstall: accept installed `clawhub:` specs and versionless ClawHub package names as uninstall targets, so `openclaw plugins uninstall clawhub:<package>` works again even when the recorded install was pinned to a version.
     - Browser/Chrome MCP: wait for existing-session browser tabs to become usable after attach instead of treating the initial Chrome MCP handshake as ready, which reduces user-profile timeouts and repeated consent churn on macOS Chrome attach flows. Fixes #52930. Thanks @vincentkoc.
     - Browser/CDP: reuse an already-running loopback browser after a short initial reachability miss instead of immediately falling back to relaunch detection, which fixes second-run browser start/open regressions on slower headless Linux setups. Fixes #53004. Thanks @vincentkoc.
    +- Agents/web_search: use the active runtime `web_search` provider instead of stale/default selection, so agent turns keep hitting the provider you actually configured. Fixes #53020. Thanks @jzakirov.
    +- Mistral/models: lower bundled Mistral max-token defaults to safe output budgets and teach `openclaw doctor --fix` to repair old persisted Mistral provider configs that still carry context-sized output limits, avoiding deterministic Mistral 422 rejects on fresh and existing setups. Fixes #52599. Thanks @vincentkoc.
     - ClawHub/macOS auth: honor macOS auth config and XDG auth paths for saved ClawHub credentials, so `openclaw skills ...` and gateway skill browsing keep using the signed-in auth state instead of silently falling back to unauthenticated mode. Fixes #53034.
     - ClawHub/macOS: read the local ClawHub login from the macOS Application Support path and still honor XDG config on macOS, so skill browsing uses the logged-in token on both default and XDG-style setups. Fixes #52949. Thanks @scoootscooob.
     - ClawHub/skills: resolve the local ClawHub auth token for gateway skill browsing and switch browse-all requests to search so ClawControl stops falling into unauthenticated 429s and empty authenticated skill lists. Fixes #52949. Thanks @vincentkoc.
    +- Config/warnings: suppress the confusing “newer OpenClaw” warning when a config written by a same-base correction release like `2026.3.23-2` is read by `2026.3.23`, while still warning for truly newer or incompatible versions.
    +- CLI/cron: make `openclaw cron add|edit --at ... --tz <iana>` honor the requested local wall-clock time for offset-less one-shot datetimes, including DST boundaries, and keep `--tz` rejected for `--every`. (#53224) Thanks @RolfHegr.
    +- Commands/auth: stop slash-command authorization from crashing or dropping valid allowlists when channel `allowFrom` resolution hits unresolved SecretRef-backed accounts, and fail closed only for the affected provider inference path. (#52791) Thanks @Lukavyi.
    +- Agents/failover: classify generic `api_error` payloads as retryable only when they include transient failure signals, so MiniMax-style backend failures still trigger model fallback without misclassifying billing, auth, or format/context errors. (#49611) Thanks @ayushozha.
    +- LINE/runtime-api: pre-export overlapping runtime symbols before the `line-runtime` star export so jiti no longer throws `TypeError: Cannot redefine property` on startup. (#53221) Thanks @Drickon.
    +- Telegram/threading: populate `currentThreadTs` in the threading tool-context fallback for Telegram DM topics so thread-aware tools still receive the active topic context when the main thread metadata is missing. (#52217)
    +- Diagnostics/cache trace: strip credential fields from cache-trace JSONL output while preserving non-sensitive diagnostic fields and image redaction metadata.
    +- Docs/Feishu: replace `botName` with `name` in the channel config examples so the docs match the strict account schema for per-account display names. (#52753) Thanks @haroldfabla2-hue.
    +- Doctor/plugins: make `openclaw doctor --fix` remove stale `plugins.allow` and `plugins.entries` refs left behind after plugin removal. Thanks @sallyom
    +- Agents/replay: canonicalize malformed assistant transcript content before session-history sanitization so legacy or corrupted assistant turns stop crashing Pi replay and subagent recovery paths.
    +- ClawHub/skills: keep updating already-tracked legacy Unicode slugs after the ASCII-only slug hardening, so older installs do not get stuck behind `Invalid skill slug` errors during `openclaw skills update`. (#53206) Thanks @drobison00.
    +- Infra/exec trust: preserve shell-multiplexer wrapper binaries for policy checks without breaking approved-command reconstruction, so BusyBox/ToyBox allowlist and audit flows bind to the real wrapper while execution plans stay coherent. (#53134) Thanks @vincentkoc.
     - Plugins/message tool: make Discord `components` and Slack `blocks` optional again, and route Feishu `message(..., media=...)` sends through the outbound media path, so pin/unpin/react flows stop failing schema validation and Feishu file/image attachments actually send. Fixes #52970 and #52962. Thanks @vincentkoc.
     - Gateway/model pricing: stop `openrouter/auto` pricing refresh from recursing indefinitely during bootstrap, so OpenRouter auto routes can populate cached pricing and `usage.cost` again. Fixes #53035. Thanks @vincentkoc.
    -- Mistral/models: lower bundled Mistral max-token defaults to safe output budgets and teach `openclaw doctor --fix` to repair old persisted Mistral provider configs that still carry context-sized output limits, avoiding deterministic Mistral 422 rejects on fresh and existing setups. Fixes #52599. Thanks @vincentkoc.
    -- Agents/web_search: use the active runtime `web_search` provider instead of stale/default selection, so agent turns keep hitting the provider you actually configured. Fixes #53020. Thanks @jzakirov.
     - Models/OpenAI Codex OAuth: bootstrap the env-configured HTTP/HTTPS proxy dispatcher on the stored-credential refresh path before token renewal runs, so expired Codex OAuth profiles can refresh successfully in proxy-required environments instead of locking users out after the first token expiry.
     - Models/OpenAI Codex OAuth and Plugins/MiniMax OAuth: ensure env-configured HTTP/HTTPS proxy dispatchers are initialized before OAuth preflight and token exchange requests so proxy-required environments can complete MiniMax and OpenAI Codex sign-in flows again. (#52228; fixes #51619, #51569) Thanks @openperf.
     - Plugins/memory-lancedb: bootstrap LanceDB into plugin runtime state on first use when the bundled npm install does not already have it, so `plugins.slots.memory="memory-lancedb"` works again after global npm installs without moving LanceDB into OpenClaw core dependencies. Fixes #26100.
    
  • docs/.generated/plugin-sdk-api-baseline.json+1 1 modified
    @@ -2620,7 +2620,7 @@
               "exportName": "resolveCommandAuthorization",
               "kind": "function",
               "source": {
    -            "line": 303,
    +            "line": 440,
                 "path": "src/auto-reply/command-auth.ts"
               }
             },
    
  • docs/.generated/plugin-sdk-api-baseline.jsonl+1 1 modified
    @@ -287,7 +287,7 @@
     {"declaration":"export function parseCommandArgs(command: ChatCommandDefinition, raw?: string | undefined): CommandArgs | undefined;","entrypoint":"command-auth","exportName":"parseCommandArgs","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":254,"sourcePath":"src/auto-reply/commands-registry.ts"}
     {"declaration":"export function resolveCommandArgChoices(params: { command: ChatCommandDefinition; arg: CommandArgDefinition; cfg?: OpenClawConfig | undefined; provider?: string | undefined; model?: string | undefined; }): ResolvedCommandArgChoice[];","entrypoint":"command-auth","exportName":"resolveCommandArgChoices","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":316,"sourcePath":"src/auto-reply/commands-registry.ts"}
     {"declaration":"export function resolveCommandArgMenu(params: { command: ChatCommandDefinition; args?: CommandArgs | undefined; cfg?: OpenClawConfig | undefined; }): { arg: CommandArgDefinition; choices: ResolvedCommandArgChoice[]; title?: string | undefined; } | null;","entrypoint":"command-auth","exportName":"resolveCommandArgMenu","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":346,"sourcePath":"src/auto-reply/commands-registry.ts"}
    -{"declaration":"export function resolveCommandAuthorization(params: { ctx: MsgContext; cfg: OpenClawConfig; commandAuthorized: boolean; }): CommandAuthorization;","entrypoint":"command-auth","exportName":"resolveCommandAuthorization","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":303,"sourcePath":"src/auto-reply/command-auth.ts"}
    +{"declaration":"export function resolveCommandAuthorization(params: { ctx: MsgContext; cfg: OpenClawConfig; commandAuthorized: boolean; }): CommandAuthorization;","entrypoint":"command-auth","exportName":"resolveCommandAuthorization","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":440,"sourcePath":"src/auto-reply/command-auth.ts"}
     {"declaration":"export function resolveCommandAuthorizedFromAuthorizers(params: { useAccessGroups: boolean; authorizers: CommandAuthorizer[]; modeWhenAccessGroupsOff?: CommandGatingModeWhenAccessGroupsOff | undefined; }): boolean;","entrypoint":"command-auth","exportName":"resolveCommandAuthorizedFromAuthorizers","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":8,"sourcePath":"src/channels/command-gating.ts"}
     {"declaration":"export function resolveControlCommandGate(params: { useAccessGroups: boolean; authorizers: CommandAuthorizer[]; allowTextCommands: boolean; hasControlCommand: boolean; modeWhenAccessGroupsOff?: CommandGatingModeWhenAccessGroupsOff | undefined; }): { ...; };","entrypoint":"command-auth","exportName":"resolveControlCommandGate","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":31,"sourcePath":"src/channels/command-gating.ts"}
     {"declaration":"export function resolveDirectDmAuthorizationOutcome(params: { isGroup: boolean; dmPolicy: string; senderAllowedForCommands: boolean; }): \"disabled\" | \"unauthorized\" | \"allowed\";","entrypoint":"command-auth","exportName":"resolveDirectDmAuthorizationOutcome","importSpecifier":"openclaw/plugin-sdk/command-auth","kind":"function","recordType":"export","sourceLine":120,"sourcePath":"src/plugin-sdk/command-auth.ts"}
    
a47722de7e3c

Integrations: tighten inbound callback and allowlist checks (#46787)

https://github.com/openclaw/openclawVincent KocMar 15, 2026via ghsa
14 files changed · +323 18
  • CHANGELOG.md+1 3 modified
    @@ -29,16 +29,14 @@ Docs: https://docs.openclaw.ai
     - Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
     - Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
     - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
    +- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. Thanks @vincentkoc.
     - macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc.
     - Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
     - Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
     - WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
     - WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
     - Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
     - Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
    -
    -### Fixes
    -
     - Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
     - Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
     - CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
    
  • extensions/googlechat/src/auth.test.ts+97 0 added
    @@ -0,0 +1,97 @@
    +import { beforeEach, describe, expect, it, vi } from "vitest";
    +
    +const mocks = vi.hoisted(() => ({
    +  verifyIdToken: vi.fn(),
    +}));
    +
    +vi.mock("google-auth-library", () => ({
    +  GoogleAuth: class {},
    +  OAuth2Client: class {
    +    verifyIdToken = mocks.verifyIdToken;
    +  },
    +}));
    +
    +const { verifyGoogleChatRequest } = await import("./auth.js");
    +
    +function mockTicket(payload: Record<string, unknown>) {
    +  mocks.verifyIdToken.mockResolvedValue({
    +    getPayload: () => payload,
    +  });
    +}
    +
    +describe("verifyGoogleChatRequest", () => {
    +  beforeEach(() => {
    +    mocks.verifyIdToken.mockReset();
    +  });
    +
    +  it("accepts Google Chat app-url tokens from the Chat issuer", async () => {
    +    mockTicket({
    +      email: "chat@system.gserviceaccount.com",
    +      email_verified: true,
    +    });
    +
    +    await expect(
    +      verifyGoogleChatRequest({
    +        bearer: "token",
    +        audienceType: "app-url",
    +        audience: "https://example.com/googlechat",
    +      }),
    +    ).resolves.toEqual({ ok: true });
    +  });
    +
    +  it("rejects add-on tokens when no principal binding is configured", async () => {
    +    mockTicket({
    +      email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
    +      email_verified: true,
    +      sub: "principal-1",
    +    });
    +
    +    await expect(
    +      verifyGoogleChatRequest({
    +        bearer: "token",
    +        audienceType: "app-url",
    +        audience: "https://example.com/googlechat",
    +      }),
    +    ).resolves.toEqual({
    +      ok: false,
    +      reason: "missing add-on principal binding",
    +    });
    +  });
    +
    +  it("accepts add-on tokens only when the bound principal matches", async () => {
    +    mockTicket({
    +      email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
    +      email_verified: true,
    +      sub: "principal-1",
    +    });
    +
    +    await expect(
    +      verifyGoogleChatRequest({
    +        bearer: "token",
    +        audienceType: "app-url",
    +        audience: "https://example.com/googlechat",
    +        expectedAddOnPrincipal: "principal-1",
    +      }),
    +    ).resolves.toEqual({ ok: true });
    +  });
    +
    +  it("rejects add-on tokens when the bound principal does not match", async () => {
    +    mockTicket({
    +      email: "service-123@gcp-sa-gsuiteaddons.iam.gserviceaccount.com",
    +      email_verified: true,
    +      sub: "principal-2",
    +    });
    +
    +    await expect(
    +      verifyGoogleChatRequest({
    +        bearer: "token",
    +        audienceType: "app-url",
    +        audience: "https://example.com/googlechat",
    +        expectedAddOnPrincipal: "principal-1",
    +      }),
    +    ).resolves.toEqual({
    +      ok: false,
    +      reason: "unexpected add-on principal: principal-2",
    +    });
    +  });
    +});
    
  • extensions/googlechat/src/auth.ts+27 4 modified
    @@ -94,6 +94,7 @@ export async function verifyGoogleChatRequest(params: {
       bearer?: string | null;
       audienceType?: GoogleChatAudienceType | null;
       audience?: string | null;
    +  expectedAddOnPrincipal?: string | null;
     }): Promise<{ ok: boolean; reason?: string }> {
       const bearer = params.bearer?.trim();
       if (!bearer) {
    @@ -112,10 +113,32 @@ export async function verifyGoogleChatRequest(params: {
             audience,
           });
           const payload = ticket.getPayload();
    -      const email = payload?.email ?? "";
    -      const ok =
    -        payload?.email_verified && (email === CHAT_ISSUER || ADDON_ISSUER_PATTERN.test(email));
    -      return ok ? { ok: true } : { ok: false, reason: `invalid issuer: ${email}` };
    +      const email = String(payload?.email ?? "")
    +        .trim()
    +        .toLowerCase();
    +      if (!payload?.email_verified) {
    +        return { ok: false, reason: "email not verified" };
    +      }
    +      if (email === CHAT_ISSUER) {
    +        return { ok: true };
    +      }
    +      if (!ADDON_ISSUER_PATTERN.test(email)) {
    +        return { ok: false, reason: `invalid issuer: ${email}` };
    +      }
    +      const expectedAddOnPrincipal = params.expectedAddOnPrincipal?.trim().toLowerCase();
    +      if (!expectedAddOnPrincipal) {
    +        return { ok: false, reason: "missing add-on principal binding" };
    +      }
    +      const tokenPrincipal = String(payload?.sub ?? "")
    +        .trim()
    +        .toLowerCase();
    +      if (!tokenPrincipal || tokenPrincipal !== expectedAddOnPrincipal) {
    +        return {
    +          ok: false,
    +          reason: `unexpected add-on principal: ${tokenPrincipal || "<missing>"}`,
    +        };
    +      }
    +      return { ok: true };
         } catch (err) {
           return { ok: false, reason: err instanceof Error ? err.message : "invalid token" };
         }
    
  • extensions/googlechat/src/monitor-webhook.ts+2 0 modified
    @@ -132,6 +132,7 @@ export function createGoogleChatWebhookRequestHandler(params: {
                     bearer: headerBearer,
                     audienceType: target.audienceType,
                     audience: target.audience,
    +                expectedAddOnPrincipal: target.account.config.appPrincipal,
                   });
                   return verification.ok;
                 },
    @@ -166,6 +167,7 @@ export function createGoogleChatWebhookRequestHandler(params: {
                     bearer: parsed.addOnBearerToken,
                     audienceType: target.audienceType,
                     audience: target.audience,
    +                expectedAddOnPrincipal: target.account.config.appPrincipal,
                   });
                   return verification.ok;
                 },
    
  • extensions/mattermost/src/mattermost/interactions.test.ts+31 0 modified
    @@ -738,6 +738,37 @@ describe("createMattermostInteractionHandler", () => {
         expectSuccessfulApprovalUpdate(res, requestLog);
       });
     
    +  it("blocks button dispatch when the sender is not allowed for the action", async () => {
    +    const { context, token } = createActionContext();
    +    const dispatchButtonClick = vi.fn();
    +    const handleInteraction = vi.fn();
    +    const handler = createMattermostInteractionHandler({
    +      client: {
    +        request: async (_path: string, init?: { method?: string }) =>
    +          init?.method === "PUT" ? { id: "post-1" } : createActionPost(),
    +      } as unknown as MattermostClient,
    +      botUserId: "bot",
    +      accountId: "acct",
    +      authorizeButtonClick: async () => ({
    +        ok: false,
    +        response: {
    +          ephemeral_text: "blocked",
    +        },
    +      }),
    +      handleInteraction,
    +      dispatchButtonClick,
    +    });
    +
    +    const res = await runHandler(handler, {
    +      body: createInteractionBody({ context, token }),
    +    });
    +
    +    expect(res.statusCode).toBe(200);
    +    expect(res.body).toContain("blocked");
    +    expect(handleInteraction).not.toHaveBeenCalled();
    +    expect(dispatchButtonClick).not.toHaveBeenCalled();
    +  });
    +
       it("forwards fetched post threading metadata to session and button callbacks", async () => {
         const enqueueSystemEvent = vi.fn();
         setMattermostRuntime({
    
  • extensions/mattermost/src/mattermost/interactions.ts+35 0 modified
    @@ -37,6 +37,10 @@ export type MattermostInteractionResponse = {
       ephemeral_text?: string;
     };
     
    +export type MattermostInteractionAuthorizationResult =
    +  | { ok: true }
    +  | { ok: false; statusCode?: number; response?: MattermostInteractionResponse };
    +
     export type MattermostInteractiveButtonInput = {
       id?: string;
       callback_data?: string;
    @@ -404,6 +408,10 @@ export function createMattermostInteractionHandler(params: {
         context: Record<string, unknown>;
         post: MattermostPost;
       }) => Promise<MattermostInteractionResponse | null>;
    +  authorizeButtonClick?: (opts: {
    +    payload: MattermostInteractionPayload;
    +    post: MattermostPost;
    +  }) => Promise<MattermostInteractionAuthorizationResult>;
       dispatchButtonClick?: (opts: {
         channelId: string;
         userId: string;
    @@ -566,6 +574,33 @@ export function createMattermostInteractionHandler(params: {
             `post=${payload.post_id} channel=${payload.channel_id}`,
         );
     
    +    if (params.authorizeButtonClick) {
    +      try {
    +        const authorization = await params.authorizeButtonClick({
    +          payload,
    +          post: originalPost,
    +        });
    +        if (!authorization.ok) {
    +          res.statusCode = authorization.statusCode ?? 200;
    +          res.setHeader("Content-Type", "application/json");
    +          res.end(
    +            JSON.stringify(
    +              authorization.response ?? {
    +                ephemeral_text: "You are not allowed to use this action here.",
    +              },
    +            ),
    +          );
    +          return;
    +        }
    +      } catch (err) {
    +        log?.(`mattermost interaction: authorization failed: ${String(err)}`);
    +        res.statusCode = 500;
    +        res.setHeader("Content-Type", "application/json");
    +        res.end(JSON.stringify({ error: "Interaction authorization failed" }));
    +        return;
    +      }
    +    }
    +
         if (params.handleInteraction) {
           try {
             const response = await params.handleInteraction({
    
  • extensions/mattermost/src/mattermost/monitor.ts+39 0 modified
    @@ -567,6 +567,45 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
           trustedProxies: cfg.gateway?.trustedProxies,
           allowRealIpFallback: cfg.gateway?.allowRealIpFallback === true,
           handleInteraction: handleModelPickerInteraction,
    +      authorizeButtonClick: async ({ payload, post }) => {
    +        const channelInfo = await resolveChannelInfo(payload.channel_id);
    +        const isDirect = channelInfo?.type?.trim().toUpperCase() === "D";
    +        const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
    +          cfg,
    +          surface: "mattermost",
    +        });
    +        const decision = authorizeMattermostCommandInvocation({
    +          account,
    +          cfg,
    +          senderId: payload.user_id,
    +          senderName: payload.user_name ?? "",
    +          channelId: payload.channel_id,
    +          channelInfo,
    +          storeAllowFrom: isDirect
    +            ? await readStoreAllowFromForDmPolicy({
    +                provider: "mattermost",
    +                accountId: account.accountId,
    +                dmPolicy: account.config.dmPolicy ?? "pairing",
    +                readStore: pairing.readStoreForDmPolicy,
    +              })
    +            : undefined,
    +          allowTextCommands,
    +          hasControlCommand: false,
    +        });
    +        if (decision.ok) {
    +          return { ok: true };
    +        }
    +        return {
    +          ok: false,
    +          response: {
    +            update: {
    +              message: post.message ?? "",
    +              props: post.props as Record<string, unknown> | undefined,
    +            },
    +            ephemeral_text: `OpenClaw ignored this action for ${decision.roomLabel}.`,
    +          },
    +        };
    +      },
           resolveSessionKey: async ({ channelId, userId, post }) => {
             const channelInfo = await resolveChannelInfo(channelId);
             const kind = mapMattermostChannelTypeToChatType(channelInfo?.type);
    
  • extensions/nextcloud-talk/src/inbound.authz.test.ts+73 0 modified
    @@ -81,4 +81,77 @@ describe("nextcloud-talk inbound authz", () => {
         });
         expect(buildMentionRegexes).not.toHaveBeenCalled();
       });
    +
    +  it("matches group rooms by token instead of colliding room names", async () => {
    +    const readAllowFromStore = vi.fn(async () => []);
    +    const buildMentionRegexes = vi.fn(() => [/@openclaw/i]);
    +
    +    setNextcloudTalkRuntime({
    +      channel: {
    +        pairing: {
    +          readAllowFromStore,
    +        },
    +        commands: {
    +          shouldHandleTextCommands: () => false,
    +        },
    +        text: {
    +          hasControlCommand: () => false,
    +        },
    +        mentions: {
    +          buildMentionRegexes,
    +          matchesMentionPatterns: () => false,
    +        },
    +      },
    +    } as unknown as PluginRuntime);
    +
    +    const message: NextcloudTalkInboundMessage = {
    +      messageId: "m-2",
    +      roomToken: "room-attacker",
    +      roomName: "Room Trusted",
    +      senderId: "trusted-user",
    +      senderName: "Trusted User",
    +      text: "hello",
    +      mediaType: "text/plain",
    +      timestamp: Date.now(),
    +      isGroupChat: true,
    +    };
    +
    +    const account: ResolvedNextcloudTalkAccount = {
    +      accountId: "default",
    +      enabled: true,
    +      baseUrl: "",
    +      secret: "",
    +      secretSource: "none",
    +      config: {
    +        dmPolicy: "pairing",
    +        allowFrom: [],
    +        groupPolicy: "allowlist",
    +        groupAllowFrom: ["trusted-user"],
    +        rooms: {
    +          "room-trusted": {
    +            enabled: true,
    +          },
    +        },
    +      },
    +    };
    +
    +    await handleNextcloudTalkInbound({
    +      message,
    +      account,
    +      config: {
    +        channels: {
    +          "nextcloud-talk": {
    +            groupPolicy: "allowlist",
    +            groupAllowFrom: ["trusted-user"],
    +          },
    +        },
    +      },
    +      runtime: {
    +        log: vi.fn(),
    +        error: vi.fn(),
    +      } as unknown as RuntimeEnv,
    +    });
    +
    +    expect(buildMentionRegexes).not.toHaveBeenCalled();
    +  });
     });
    
  • extensions/nextcloud-talk/src/inbound.ts+0 1 modified
    @@ -114,7 +114,6 @@ export async function handleNextcloudTalkInbound(params: {
       const roomMatch = resolveNextcloudTalkRoomMatch({
         rooms: account.config.rooms,
         roomToken,
    -    roomName,
       });
       const roomConfig = roomMatch.roomConfig;
       if (isGroup && !roomMatch.allowed) {
    
  • extensions/nextcloud-talk/src/policy.ts+1 9 modified
    @@ -57,16 +57,10 @@ export type NextcloudTalkRoomMatch = {
     export function resolveNextcloudTalkRoomMatch(params: {
       rooms?: Record<string, NextcloudTalkRoomConfig>;
       roomToken: string;
    -  roomName?: string | null;
     }): NextcloudTalkRoomMatch {
       const rooms = params.rooms ?? {};
       const allowlistConfigured = Object.keys(rooms).length > 0;
    -  const roomName = params.roomName?.trim() || undefined;
    -  const roomCandidates = buildChannelKeyCandidates(
    -    params.roomToken,
    -    roomName,
    -    roomName ? normalizeChannelSlug(roomName) : undefined,
    -  );
    +  const roomCandidates = buildChannelKeyCandidates(params.roomToken);
       const match = resolveChannelEntryMatchWithFallback({
         entries: rooms,
         keys: roomCandidates,
    @@ -101,11 +95,9 @@ export function resolveNextcloudTalkGroupToolPolicy(
       if (!roomToken) {
         return undefined;
       }
    -  const roomName = params.groupChannel?.trim() || undefined;
       const match = resolveNextcloudTalkRoomMatch({
         rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
         roomToken,
    -    roomName,
       });
       return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
     }
    
  • extensions/twitch/src/access-control.test.ts+7 0 modified
    @@ -160,6 +160,13 @@ describe("checkTwitchAccessControl", () => {
           });
         });
     
    +    it("blocks everyone when allowFrom is explicitly empty", () => {
    +      expectAllowFromBlocked({
    +        allowFrom: [],
    +        reason: "allowFrom",
    +      });
    +    });
    +
         it("blocks messages without userId", () => {
           expectAllowFromBlocked({
             allowFrom: ["123456"],
    
  • extensions/twitch/src/access-control.ts+7 1 modified
    @@ -48,8 +48,14 @@ export function checkTwitchAccessControl(params: {
         }
       }
     
    -  if (account.allowFrom && account.allowFrom.length > 0) {
    +  if (account.allowFrom !== undefined) {
         const allowFrom = account.allowFrom;
    +    if (allowFrom.length === 0) {
    +      return {
    +        allowed: false,
    +        reason: "sender is not in allowFrom allowlist",
    +      };
    +    }
         const senderId = message.userId;
     
         if (!senderId) {
    
  • src/config/types.googlechat.ts+2 0 modified
    @@ -75,6 +75,8 @@ export type GoogleChatAccountConfig = {
       audienceType?: "app-url" | "project-number";
       /** Audience value (app URL or project number). */
       audience?: string;
    +  /** Exact add-on principal to accept when app-url delivery uses add-on tokens. */
    +  appPrincipal?: string;
       /** Google Chat webhook path (default: /googlechat). */
       webhookPath?: string;
       /** Google Chat webhook URL (used to derive the path). */
    
  • src/config/zod-schema.providers-core.ts+1 0 modified
    @@ -767,6 +767,7 @@ export const GoogleChatAccountSchema = z
         serviceAccountFile: z.string().optional(),
         audienceType: z.enum(["app-url", "project-number"]).optional(),
         audience: z.string().optional(),
    +    appPrincipal: z.string().optional(),
         webhookPath: z.string().optional(),
         webhookUrl: z.string().optional(),
         botUser: z.string().optional(),
    

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.