High severityNVD Advisory· Published Mar 29, 2026· Updated Apr 1, 2026
OpenClaw < 2026.3.12 - Forged Event Injection via Feishu Webhook Verification Token
CVE-2026-32974
Description
OpenClaw before 2026.3.12 contains an authentication bypass vulnerability in Feishu webhook mode when only verificationToken is configured without encryptKey, allowing acceptance of forged events. Unauthenticated network attackers can inject forged Feishu events and trigger downstream tool execution by reaching the webhook endpoint.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.12 | 2026.3.12 |
Affected products
1Patches
17844bc89a161Security: require Feishu webhook encrypt key (#44087)
13 files changed · +254 −18
CHANGELOG.md+1 −0 modified@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Security/browser.request: block persistent browser profile create/delete routes from write-scoped `browser.request` so callers can no longer persist admin-only browser profile changes through the browser control surface. (`GHSA-vmhq-cqm9-6p7q`)(#43800) Thanks @tdjackey and @vincentkoc. - Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external `agent` callers can no longer override the gateway workspace boundary. (`GHSA-2rqg-gjgv-84jm`)(#43801) Thanks @tdjackey and @vincentkoc. - Security/exec allowlist: preserve POSIX case sensitivity and keep `?` within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (`GHSA-f8r2-vg7x-gh8m`)(#43798) Thanks @zpbrent and @vincentkoc. +- Security/Feishu webhook: require `encryptKey` alongside `verificationToken` in webhook mode so unsigned forged events are rejected instead of being processed with token-only configuration. (`GHSA-g353-mgv3-8pcj`)(#44087) Thanks @lintsinghua and @vincentkoc. - Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (`GHSA-9r3v-37xh-2cf6`)(#44091) Thanks @wooluo and @vincentkoc. - Security/WebSocket preauth: shorten unauthenticated handshake retention and reject oversized pre-auth frames before application-layer parsing to reduce pre-pairing exposure on unsupported public deployments. (`GHSA-jv4g-m82p-2j93`)(#44089) (`GHSA-xwx2-ppv2-wx98`)(#44089) Thanks @ez-lbz and @vincentkoc. - Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic `p2p` reactions. (`GHSA-m69h-jm2f-2pv8`)(#44088) Thanks @zpbrent and @vincentkoc.
docs/channels/feishu.md+7 −4 modified@@ -193,16 +193,18 @@ Edit `~/.openclaw/openclaw.json`: } ``` -If you use `connectionMode: "webhook"`, set `verificationToken`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address. +If you use `connectionMode: "webhook"`, set both `verificationToken` and `encryptKey`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address. -#### Verification Token (webhook mode) +#### Verification Token and Encrypt Key (webhook mode) -When using webhook mode, set `channels.feishu.verificationToken` in your config. To get the value: +When using webhook mode, set both `channels.feishu.verificationToken` and `channels.feishu.encryptKey` in your config. To get the values: 1. In Feishu Open Platform, open your app 2. Go to **Development** → **Events & Callbacks** (开发配置 → 事件与回调) 3. Open the **Encryption** tab (加密策略) -4. Copy **Verification Token** +4. Copy **Verification Token** and **Encrypt Key** + +The screenshot below shows where to find the **Verification Token**. The **Encrypt Key** is listed in the same **Encryption** section.  @@ -600,6 +602,7 @@ Key options: | `channels.feishu.connectionMode` | Event transport mode | `websocket` | | `channels.feishu.defaultAccount` | Default account ID for outbound routing | `default` | | `channels.feishu.verificationToken` | Required for webhook mode | - | +| `channels.feishu.encryptKey` | Required for webhook mode | - | | `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` | | `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` | | `channels.feishu.webhookPort` | Webhook bind port | `3000` |
extensions/feishu/src/accounts.test.ts+19 −0 modified@@ -241,6 +241,25 @@ describe("resolveFeishuCredentials", () => { domain: "feishu", }); }); + + it("does not resolve encryptKey SecretRefs outside webhook mode", () => { + const creds = resolveFeishuCredentials( + asConfig({ + connectionMode: "websocket", + appId: "cli_123", + appSecret: "secret_456", + encryptKey: { source: "file", provider: "default", id: "path/to/secret" } as never, + }), + ); + + expect(creds).toEqual({ + appId: "cli_123", + appSecret: "secret_456", // pragma: allowlist secret + encryptKey: undefined, + verificationToken: undefined, + domain: "feishu", + }); + }); }); describe("resolveFeishuAccount", () => {
extensions/feishu/src/accounts.ts+5 −1 modified@@ -169,10 +169,14 @@ export function resolveFeishuCredentials( if (!appId || !appSecret) { return null; } + const connectionMode = cfg?.connectionMode ?? "websocket"; return { appId, appSecret, - encryptKey: normalizeString(cfg?.encryptKey), + encryptKey: + connectionMode === "webhook" + ? resolveSecretLike(cfg?.encryptKey, "channels.feishu.encryptKey") + : normalizeString(cfg?.encryptKey), verificationToken: resolveSecretLike( cfg?.verificationToken, "channels.feishu.verificationToken",
extensions/feishu/src/channel.ts+2 −2 modified@@ -129,7 +129,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { defaultAccount: { type: "string" }, appId: { type: "string" }, appSecret: secretInputJsonSchema, - encryptKey: { type: "string" }, + encryptKey: secretInputJsonSchema, verificationToken: secretInputJsonSchema, domain: { oneOf: [ @@ -170,7 +170,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { name: { type: "string" }, appId: { type: "string" }, appSecret: secretInputJsonSchema, - encryptKey: { type: "string" }, + encryptKey: secretInputJsonSchema, verificationToken: secretInputJsonSchema, domain: { type: "string", enum: ["feishu", "lark"] }, connectionMode: { type: "string", enum: ["websocket", "webhook"] },
extensions/feishu/src/config-schema.test.ts+63 −2 modified@@ -47,14 +47,29 @@ describe("FeishuConfigSchema webhook validation", () => { } }); - it("accepts top-level webhook mode with verificationToken", () => { + it("rejects top-level webhook mode without encryptKey", () => { const result = FeishuConfigSchema.safeParse({ connectionMode: "webhook", verificationToken: "token_top", appId: "cli_top", appSecret: "secret_top", // pragma: allowlist secret }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some((issue) => issue.path.join(".") === "encryptKey")).toBe(true); + } + }); + + it("accepts top-level webhook mode with verificationToken and encryptKey", () => { + const result = FeishuConfigSchema.safeParse({ + connectionMode: "webhook", + verificationToken: "token_top", + encryptKey: "encrypt_top", + appId: "cli_top", + appSecret: "secret_top", // pragma: allowlist secret + }); + expect(result.success).toBe(true); }); @@ -79,9 +94,30 @@ describe("FeishuConfigSchema webhook validation", () => { } }); - it("accepts account webhook mode inheriting top-level verificationToken", () => { + it("rejects account webhook mode without encryptKey", () => { + const result = FeishuConfigSchema.safeParse({ + accounts: { + main: { + connectionMode: "webhook", + verificationToken: "token_main", + appId: "cli_main", + appSecret: "secret_main", // pragma: allowlist secret + }, + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some((issue) => issue.path.join(".") === "accounts.main.encryptKey"), + ).toBe(true); + } + }); + + it("accepts account webhook mode inheriting top-level verificationToken and encryptKey", () => { const result = FeishuConfigSchema.safeParse({ verificationToken: "token_top", + encryptKey: "encrypt_top", accounts: { main: { connectionMode: "webhook", @@ -102,6 +138,31 @@ describe("FeishuConfigSchema webhook validation", () => { provider: "default", id: "FEISHU_VERIFICATION_TOKEN", }, + encryptKey: "encrypt_top", + appId: "cli_top", + appSecret: { + source: "env", + provider: "default", + id: "FEISHU_APP_SECRET", + }, + }); + + expect(result.success).toBe(true); + }); + + it("accepts SecretRef encryptKey in webhook mode", () => { + const result = FeishuConfigSchema.safeParse({ + connectionMode: "webhook", + verificationToken: { + source: "env", + provider: "default", + id: "FEISHU_VERIFICATION_TOKEN", + }, + encryptKey: { + source: "env", + provider: "default", + id: "FEISHU_ENCRYPT_KEY", + }, appId: "cli_top", appSecret: { source: "env",
extensions/feishu/src/config-schema.ts+30 −9 modified@@ -186,7 +186,7 @@ export const FeishuAccountConfigSchema = z name: z.string().optional(), // Display name for this account appId: z.string().optional(), appSecret: buildSecretInputSchema().optional(), - encryptKey: z.string().optional(), + encryptKey: buildSecretInputSchema().optional(), verificationToken: buildSecretInputSchema().optional(), domain: FeishuDomainSchema.optional(), connectionMode: FeishuConnectionModeSchema.optional(), @@ -204,7 +204,7 @@ export const FeishuConfigSchema = z // Top-level credentials (backward compatible for single-account mode) appId: z.string().optional(), appSecret: buildSecretInputSchema().optional(), - encryptKey: z.string().optional(), + encryptKey: buildSecretInputSchema().optional(), verificationToken: buildSecretInputSchema().optional(), domain: FeishuDomainSchema.optional().default("feishu"), connectionMode: FeishuConnectionModeSchema.optional().default("websocket"), @@ -240,13 +240,23 @@ export const FeishuConfigSchema = z const defaultConnectionMode = value.connectionMode ?? "websocket"; const defaultVerificationTokenConfigured = hasConfiguredSecretInput(value.verificationToken); - if (defaultConnectionMode === "webhook" && !defaultVerificationTokenConfigured) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["verificationToken"], - message: - 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken', - }); + const defaultEncryptKeyConfigured = hasConfiguredSecretInput(value.encryptKey); + if (defaultConnectionMode === "webhook") { + if (!defaultVerificationTokenConfigured) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["verificationToken"], + message: + 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken', + }); + } + if (!defaultEncryptKeyConfigured) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["encryptKey"], + message: 'channels.feishu.connectionMode="webhook" requires channels.feishu.encryptKey', + }); + } } for (const [accountId, account] of Object.entries(value.accounts ?? {})) { @@ -259,6 +269,8 @@ export const FeishuConfigSchema = z } const accountVerificationTokenConfigured = hasConfiguredSecretInput(account.verificationToken) || defaultVerificationTokenConfigured; + const accountEncryptKeyConfigured = + hasConfiguredSecretInput(account.encryptKey) || defaultEncryptKeyConfigured; if (!accountVerificationTokenConfigured) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -268,6 +280,15 @@ export const FeishuConfigSchema = z "a verificationToken (account-level or top-level)", }); } + if (!accountEncryptKeyConfigured) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["accounts", accountId, "encryptKey"], + message: + `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires ` + + "an encryptKey (account-level or top-level)", + }); + } } if (value.dmPolicy === "open") {
extensions/feishu/src/monitor.account.ts+3 −0 modified@@ -534,6 +534,9 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams): if (connectionMode === "webhook" && !account.verificationToken?.trim()) { throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`); } + if (connectionMode === "webhook" && !account.encryptKey?.trim()) { + throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`); + } const warmupCount = await warmupDedupFromDisk(accountId, log); if (warmupCount > 0) {
extensions/feishu/src/monitor.webhook-security.test.ts+19 −0 modified@@ -64,6 +64,7 @@ function buildConfig(params: { path: string; port: number; verificationToken?: string; + encryptKey?: string; }): ClawdbotConfig { return { channels: { @@ -78,6 +79,7 @@ function buildConfig(params: { webhookHost: "127.0.0.1", webhookPort: params.port, webhookPath: params.path, + encryptKey: params.encryptKey, verificationToken: params.verificationToken, }, }, @@ -91,6 +93,7 @@ async function withRunningWebhookMonitor( accountId: string; path: string; verificationToken: string; + encryptKey: string; }, run: (url: string) => Promise<void>, ) { @@ -99,6 +102,7 @@ async function withRunningWebhookMonitor( accountId: params.accountId, path: params.path, port, + encryptKey: params.encryptKey, verificationToken: params.verificationToken, }); @@ -141,13 +145,27 @@ describe("Feishu webhook security hardening", () => { ); }); + it("rejects webhook mode without encryptKey", async () => { + probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); + + const cfg = buildConfig({ + accountId: "missing-encrypt-key", + path: "/hook-missing-encrypt", + port: await getFreePort(), + verificationToken: "verify_token", + }); + + await expect(monitorFeishuProvider({ config: cfg })).rejects.toThrow(/requires encryptKey/i); + }); + it("returns 415 for POST requests without json content type", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); await withRunningWebhookMonitor( { accountId: "content-type", path: "/hook-content-type", verificationToken: "verify_token", + encryptKey: "encrypt_key", }, async (url) => { const response = await fetch(url, { @@ -169,6 +187,7 @@ describe("Feishu webhook security hardening", () => { accountId: "rate-limit", path: "/hook-rate-limit", verificationToken: "verify_token", + encryptKey: "encrypt_key", }, async (url) => { let saw429 = false;
extensions/feishu/src/onboarding.ts+31 −0 modified@@ -370,6 +370,37 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = { }, }; } + const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey; + const encryptKeyPromptState = buildSingleChannelSecretPromptState({ + accountConfigured: hasConfiguredSecretInput(currentEncryptKey), + hasConfigToken: hasConfiguredSecretInput(currentEncryptKey), + allowEnv: false, + }); + const encryptKeyResult = await promptSingleChannelSecretInput({ + cfg: next, + prompter, + providerHint: "feishu-webhook", + credentialLabel: "encrypt key", + accountConfigured: encryptKeyPromptState.accountConfigured, + canUseEnv: encryptKeyPromptState.canUseEnv, + hasConfigToken: encryptKeyPromptState.hasConfigToken, + envPrompt: "", + keepPrompt: "Feishu encrypt key already configured. Keep it?", + inputPrompt: "Enter Feishu encrypt key", + preferredEnvVar: "FEISHU_ENCRYPT_KEY", + }); + if (encryptKeyResult.action === "set") { + next = { + ...next, + channels: { + ...next.channels, + feishu: { + ...next.channels?.feishu, + encryptKey: encryptKeyResult.value, + }, + }, + }; + } const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath; const webhookPath = String( await prompter.text({
src/secrets/runtime-config-collectors-channels.ts+42 −0 modified@@ -801,6 +801,31 @@ function collectFeishuAssignments(params: { : baseConnectionMode; return accountMode === "webhook"; }); + const topLevelEncryptKeyActive = !surface.channelEnabled + ? false + : !surface.hasExplicitAccounts + ? baseConnectionMode === "webhook" + : surface.accounts.some(({ account, enabled }) => { + if (!enabled || hasOwnProperty(account, "encryptKey")) { + return false; + } + const accountMode = hasOwnProperty(account, "connectionMode") + ? normalizeSecretStringValue(account.connectionMode) + : baseConnectionMode; + return accountMode === "webhook"; + }); + collectSecretInputAssignment({ + value: feishu.encryptKey, + path: "channels.feishu.encryptKey", + expected: "string", + defaults: params.defaults, + context: params.context, + active: topLevelEncryptKeyActive, + inactiveReason: "no enabled Feishu webhook-mode surface inherits this top-level encryptKey.", + apply: (value) => { + feishu.encryptKey = value; + }, + }); collectSecretInputAssignment({ value: feishu.verificationToken, path: "channels.feishu.verificationToken", @@ -818,6 +843,23 @@ function collectFeishuAssignments(params: { return; } for (const { accountId, account, enabled } of surface.accounts) { + if (hasOwnProperty(account, "encryptKey")) { + const accountMode = hasOwnProperty(account, "connectionMode") + ? normalizeSecretStringValue(account.connectionMode) + : baseConnectionMode; + collectSecretInputAssignment({ + value: account.encryptKey, + path: `channels.feishu.accounts.${accountId}.encryptKey`, + expected: "string", + defaults: params.defaults, + context: params.context, + active: enabled && accountMode === "webhook", + inactiveReason: "Feishu account is disabled or not running in webhook mode.", + apply: (value) => { + account.encryptKey = value; + }, + }); + } if (!hasOwnProperty(account, "verificationToken")) { continue; }
src/secrets/runtime.coverage.test.ts+10 −0 modified@@ -71,13 +71,23 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string) if (entry.id === "channels.feishu.verificationToken") { setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook"); } + if (entry.id === "channels.feishu.encryptKey") { + setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook"); + } if (entry.id === "channels.feishu.accounts.*.verificationToken") { setPathCreateStrict( config, ["channels", "feishu", "accounts", "sample", "connectionMode"], "webhook", ); } + if (entry.id === "channels.feishu.accounts.*.encryptKey") { + setPathCreateStrict( + config, + ["channels", "feishu", "accounts", "sample", "connectionMode"], + "webhook", + ); + } if (entry.id === "tools.web.search.gemini.apiKey") { setPathCreateStrict(config, ["tools", "web", "search", "provider"], "gemini"); }
src/secrets/target-registry-data.ts+22 −0 modified@@ -173,6 +173,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "channels.feishu.accounts.*.encryptKey", + targetType: "channels.feishu.accounts.*.encryptKey", + configFile: "openclaw.json", + pathPattern: "channels.feishu.accounts.*.encryptKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "channels.feishu.accounts.*.verificationToken", targetType: "channels.feishu.accounts.*.verificationToken", @@ -195,6 +206,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "channels.feishu.encryptKey", + targetType: "channels.feishu.encryptKey", + configFile: "openclaw.json", + pathPattern: "channels.feishu.encryptKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "channels.feishu.verificationToken", targetType: "channels.feishu.verificationToken",
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
7- github.com/advisories/GHSA-g353-mgv3-8pcjghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-g353-mgv3-8pcjghsathird-party-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-32974ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-forged-event-injection-via-feishu-webhook-verification-tokenghsathird-party-advisoryWEB
- github.com/openclaw/openclaw/commit/7844bc89a1612800810617c823eb0c76ef945804ghsaWEB
- github.com/openclaw/openclaw/pull/44087ghsaWEB
- github.com/openclaw/openclaw/releases/tag/v2026.3.12ghsaWEB
News mentions
0No linked articles in our index yet.