VYPR
High severity7.5NVD Advisory· Published Mar 29, 2026· Updated Mar 31, 2026

CVE-2026-32980

CVE-2026-32980

Description

OpenClaw before 2026.3.13 reads and buffers Telegram webhook request bodies before validating the x-telegram-bot-api-secret-token header, allowing unauthenticated attackers to exhaust server resources. Attackers can send POST requests to the webhook endpoint to force memory consumption, socket time, and JSON parsing work before authentication validation occurs.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.132026.3.13

Affected products

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

Patches

1
7e49e98f7907

fix(telegram): validate webhook secret before reading request body

https://github.com/openclaw/openclawRobin WaslanderMar 13, 2026via ghsa
3 files changed · +123 3
  • CHANGELOG.md+1 0 modified
    @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
     
     ### Fixes
     
    +- Telegram/webhook auth: validate the Telegram webhook secret before reading or parsing request bodies, so unauthenticated requests are rejected immediately instead of consuming up to 1 MB first. Thanks @space08.
     - Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.
     - Browser/existing-session: accept text-only `list_pages` and `new_page` responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata.
     - Ollama/reasoning visibility: stop promoting native `thinking` and `reasoning` fields into final assistant text so local reasoning models no longer leak internal thoughts in normal replies. (#45330) Thanks @xi7ang.
    
  • src/telegram/webhook.test.ts+92 1 modified
    @@ -88,6 +88,70 @@ async function postWebhookJson(params: {
       );
     }
     
    +async function postWebhookHeadersOnly(params: {
    +  port: number;
    +  path: string;
    +  declaredLength: number;
    +  secret?: string;
    +  timeoutMs?: number;
    +}): Promise<{ statusCode: number; body: string }> {
    +  return await new Promise((resolve, reject) => {
    +    let settled = false;
    +    const finishResolve = (value: { statusCode: number; body: string }) => {
    +      if (settled) {
    +        return;
    +      }
    +      settled = true;
    +      clearTimeout(timeout);
    +      resolve(value);
    +    };
    +    const finishReject = (error: unknown) => {
    +      if (settled) {
    +        return;
    +      }
    +      settled = true;
    +      clearTimeout(timeout);
    +      reject(error);
    +    };
    +
    +    const req = request(
    +      {
    +        hostname: "127.0.0.1",
    +        port: params.port,
    +        path: params.path,
    +        method: "POST",
    +        headers: {
    +          "content-type": "application/json",
    +          "content-length": String(params.declaredLength),
    +          ...(params.secret ? { "x-telegram-bot-api-secret-token": params.secret } : {}),
    +        },
    +      },
    +      (res) => {
    +        collectResponseBody(res, (payload) => {
    +          finishResolve(payload);
    +          req.destroy();
    +        });
    +      },
    +    );
    +
    +    const timeout = setTimeout(() => {
    +      req.destroy(
    +        new Error(`webhook header-only post timed out after ${params.timeoutMs ?? 5_000}ms`),
    +      );
    +      finishReject(new Error("timed out waiting for webhook response"));
    +    }, params.timeoutMs ?? 5_000);
    +
    +    req.on("error", (error) => {
    +      if (settled && (error as NodeJS.ErrnoException).code === "ECONNRESET") {
    +        return;
    +      }
    +      finishReject(error);
    +    });
    +
    +    req.flushHeaders();
    +  });
    +}
    +
     function createDeterministicRng(seed: number): () => number {
       let state = seed >>> 0;
       return () => {
    @@ -399,7 +463,34 @@ describe("startTelegramWebhook", () => {
               secret: TELEGRAM_SECRET,
             });
             expect(response.status).toBe(200);
    -        expect(handlerSpy).toHaveBeenCalled();
    +        expect(handlerSpy).toHaveBeenCalledWith(
    +          JSON.parse(payload),
    +          expect.any(Function),
    +          TELEGRAM_SECRET,
    +          expect.any(Function),
    +        );
    +      },
    +    );
    +  });
    +
    +  it("rejects unauthenticated requests before reading the request body", async () => {
    +    handlerSpy.mockClear();
    +    await withStartedWebhook(
    +      {
    +        secret: TELEGRAM_SECRET,
    +        path: TELEGRAM_WEBHOOK_PATH,
    +      },
    +      async ({ port }) => {
    +        const response = await postWebhookHeadersOnly({
    +          port,
    +          path: TELEGRAM_WEBHOOK_PATH,
    +          declaredLength: 1_024 * 1_024,
    +          secret: "wrong-secret",
    +        });
    +
    +        expect(response.statusCode).toBe(401);
    +        expect(response.body).toBe("unauthorized");
    +        expect(handlerSpy).not.toHaveBeenCalled();
           },
         );
       });
    
  • src/telegram/webhook.ts+30 2 modified
    @@ -1,3 +1,4 @@
    +import { timingSafeEqual } from "node:crypto";
     import { createServer } from "node:http";
     import { InputFile, webhookCallback } from "grammy";
     import type { OpenClawConfig } from "../config/config.js";
    @@ -74,6 +75,28 @@ async function initializeTelegramWebhookBot(params: {
       });
     }
     
    +function resolveSingleHeaderValue(header: string | string[] | undefined): string | undefined {
    +  if (typeof header === "string") {
    +    return header;
    +  }
    +  if (Array.isArray(header) && header.length === 1) {
    +    return header[0];
    +  }
    +  return undefined;
    +}
    +
    +function hasValidTelegramWebhookSecret(
    +  secretHeader: string | undefined,
    +  expectedSecret: string,
    +): boolean {
    +  if (typeof secretHeader !== "string") {
    +    return false;
    +  }
    +  const actual = Buffer.from(secretHeader, "utf-8");
    +  const expected = Buffer.from(expectedSecret, "utf-8");
    +  return actual.length === expected.length && timingSafeEqual(actual, expected);
    +}
    +
     export async function startTelegramWebhook(opts: {
       token: string;
       accountId?: string;
    @@ -147,6 +170,13 @@ export async function startTelegramWebhook(opts: {
         if (diagnosticsEnabled) {
           logWebhookReceived({ channel: "telegram", updateType: "telegram-post" });
         }
    +    const secretHeader = resolveSingleHeaderValue(req.headers["x-telegram-bot-api-secret-token"]);
    +    if (!hasValidTelegramWebhookSecret(secretHeader, secret)) {
    +      res.shouldKeepAlive = false;
    +      res.setHeader("Connection", "close");
    +      respondText(401, "unauthorized");
    +      return;
    +    }
         void (async () => {
           const body = await readJsonBodyWithLimit(req, {
             maxBytes: TELEGRAM_WEBHOOK_MAX_BODY_BYTES,
    @@ -189,8 +219,6 @@ export async function startTelegramWebhook(opts: {
             replied = true;
             respondText(401, "unauthorized");
           };
    -      const secretHeaderRaw = req.headers["x-telegram-bot-api-secret-token"];
    -      const secretHeader = Array.isArray(secretHeaderRaw) ? secretHeaderRaw[0] : secretHeaderRaw;
     
           await handler(body.value, reply, secretHeader, unauthorized);
           if (!replied) {
    

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

5

News mentions

0

No linked articles in our index yet.