Medium severity6.5NVD Advisory· Published Mar 31, 2026· Updated Apr 1, 2026
CVE-2026-33580
CVE-2026-33580
Description
OpenClaw before 2026.3.28 contains a missing rate limiting vulnerability in the Nextcloud Talk webhook authentication that allows attackers to brute-force weak shared secrets. Attackers who can reach the webhook endpoint can exploit this to forge inbound webhook events by repeatedly attempting authentication without throttling.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.28 | 2026.3.28 |
Affected products
1Patches
1e403decb6e20nextcloud-talk: throttle repeated webhook auth failures (#56007)
3 files changed · +81 −1
extensions/nextcloud-talk/src/monitor.replay.test.ts+55 −0 modified@@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { createMockIncomingRequest } from "../../../test/helpers/mock-incoming-request.js"; +import { WEBHOOK_RATE_LIMIT_DEFAULTS } from "../runtime-api.js"; import { readNextcloudTalkWebhookBody } from "./monitor.js"; import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js"; import { startWebhookServer } from "./monitor.test-harness.js"; @@ -145,3 +146,57 @@ describe("createNextcloudTalkWebhookServer payload validation", () => { expect(await response.json()).toEqual({ error: "Invalid payload format" }); }); }); + +describe("createNextcloudTalkWebhookServer auth rate limiting", () => { + it("rate limits repeated invalid signature attempts from the same source", async () => { + const harness = await startWebhookServer({ + path: "/nextcloud-auth-rate-limit", + onMessage: vi.fn(), + }); + const { body, headers } = createSignedCreateMessageRequest(); + const invalidHeaders = { + ...headers, + "x-nextcloud-talk-signature": "invalid-signature", + }; + + let firstResponse: Response | undefined; + let lastResponse: Response | undefined; + for (let attempt = 0; attempt <= WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests; attempt += 1) { + const response = await fetch(harness.webhookUrl, { + method: "POST", + headers: invalidHeaders, + body, + }); + if (attempt === 0) { + firstResponse = response; + } + lastResponse = response; + } + + expect(firstResponse).toBeDefined(); + expect(firstResponse?.status).toBe(401); + expect(lastResponse).toBeDefined(); + expect(lastResponse?.status).toBe(429); + expect(await lastResponse?.text()).toBe("Too Many Requests"); + }); + + it("does not rate limit valid signed webhook bursts from the same source", async () => { + const harness = await startWebhookServer({ + path: "/nextcloud-auth-rate-limit-valid", + onMessage: vi.fn(), + }); + const { body, headers } = createSignedCreateMessageRequest(); + + let lastResponse: Response | undefined; + for (let attempt = 0; attempt <= WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests; attempt += 1) { + lastResponse = await fetch(harness.webhookUrl, { + method: "POST", + headers, + body, + }); + } + + expect(lastResponse).toBeDefined(); + expect(lastResponse?.status).toBe(200); + }); +});
extensions/nextcloud-talk/src/monitor.ts+23 −0 modified@@ -6,6 +6,8 @@ import { } from "openclaw/plugin-sdk/extension-shared"; import { z } from "zod"; import { + WEBHOOK_RATE_LIMIT_DEFAULTS, + createAuthRateLimiter, type RuntimeEnv, isRequestBodyLimitError, readRequestBodyWithLimit, @@ -32,6 +34,7 @@ const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000; const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024; const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5_000; const HEALTH_PATH = "/healthz"; +const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth"; const NextcloudTalkWebhookPayloadSchema: z.ZodType<NextcloudTalkWebhookPayload> = z.object({ type: z.enum(["Create", "Update", "Delete"]), actor: z.object({ @@ -125,6 +128,8 @@ function verifyWebhookSignature(params: { body: string; secret: string; res: ServerResponse; + clientIp: string; + authRateLimiter: ReturnType<typeof createAuthRateLimiter>; }): boolean { const isValid = verifyNextcloudTalkSignature({ signature: params.headers.signature, @@ -133,9 +138,11 @@ function verifyWebhookSignature(params: { secret: params.secret, }); if (!isValid) { + params.authRateLimiter.recordFailure(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE); writeWebhookError(params.res, 401, WEBHOOK_ERRORS.invalidSignature); return false; } + params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE); return true; } @@ -203,6 +210,13 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe const readBody = opts.readBody ?? readNextcloudTalkWebhookBody; const isBackendAllowed = opts.isBackendAllowed; const shouldProcessMessage = opts.shouldProcessMessage; + const webhookAuthRateLimiter = createAuthRateLimiter({ + maxAttempts: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests, + windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs, + lockoutMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs, + exemptLoopback: false, + pruneIntervalMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs, + }); const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { if (req.url === HEALTH_PATH) { @@ -217,6 +231,13 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe return; } + const clientIp = req.socket.remoteAddress ?? "unknown"; + if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) { + res.writeHead(429); + res.end("Too Many Requests"); + return; + } + try { const headers = validateWebhookHeaders({ req, @@ -234,6 +255,8 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe body, secret, res, + clientIp, + authRateLimiter: webhookAuthRateLimiter, }); if (!hasValidSignature) { return;
src/plugin-sdk/nextcloud-talk.ts+3 −1 modified@@ -2,6 +2,7 @@ // Keep this list additive and scoped to symbols used under extensions/nextcloud-talk. export { logInboundDrop } from "../channels/logging.js"; +export { createAuthRateLimiter } from "../gateway/auth-rate-limit.js"; export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; export { @@ -70,10 +71,11 @@ export { requireOpenAllowFrom, } from "../config/zod-schema.core.js"; export { + WEBHOOK_RATE_LIMIT_DEFAULTS, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "../infra/http-body.js"; +} from "./webhook-ingress.js"; export { waitForAbortSignal } from "../infra/abort-signal.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
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- github.com/openclaw/openclaw/commit/e403decb6e20091b5402780a7ccd2085f98aa3cdnvdPatchWEB
- github.com/advisories/GHSA-9528-x887-j2fpghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-9528-x887-j2fpnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-33580ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-brute-force-attack-via-missing-rate-limiting-on-webhook-shared-secret-authenticationnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/releases/tag/v2026.3.28ghsaWEB
News mentions
0No linked articles in our index yet.