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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.13 | 2026.3.13 |
Affected products
1Patches
17e49e98f7907fix(telegram): validate webhook secret before reading request body
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- github.com/openclaw/openclaw/commit/7e49e98f79073b11134beac27fdff547ba5a4a02nvdPatchWEB
- github.com/advisories/GHSA-jq3f-vjww-8rq7ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-jq3f-vjww-8rq7nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-32980ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-resource-exhaustion-via-unauthenticated-telegram-webhook-requestnvdThird Party AdvisoryWEB
News mentions
0No linked articles in our index yet.