VYPR
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.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

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

Patches

1
e403decb6e20

nextcloud-talk: throttle repeated webhook auth failures (#56007)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.