fix(security): enforce bounded webhook body handling

This commit is contained in:
Peter Steinberger
2026-02-13 19:14:36 +01:00
parent 2f9c523bbe
commit 3cbcba10cf
20 changed files with 834 additions and 281 deletions

View File

@@ -1,6 +1,11 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions, resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
import {
createReplyPrefixOptions,
readJsonBodyWithLimit,
requestBodyErrorToText,
resolveMentionGatingWithBypass,
} from "openclaw/plugin-sdk";
import type {
GoogleChatAnnotation,
GoogleChatAttachment,
@@ -84,46 +89,6 @@ function resolveWebhookPath(webhookPath?: string, webhookUrl?: string): string |
return "/googlechat";
}
async function readJsonBody(req: IncomingMessage, maxBytes: number) {
const chunks: Buffer[] = [];
let total = 0;
return await new Promise<{ ok: boolean; value?: unknown; error?: string }>((resolve) => {
let resolved = false;
const doResolve = (value: { ok: boolean; value?: unknown; error?: string }) => {
if (resolved) {
return;
}
resolved = true;
req.removeAllListeners();
resolve(value);
};
req.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > maxBytes) {
doResolve({ ok: false, error: "payload too large" });
req.destroy();
return;
}
chunks.push(chunk);
});
req.on("end", () => {
try {
const raw = Buffer.concat(chunks).toString("utf8");
if (!raw.trim()) {
doResolve({ ok: false, error: "empty payload" });
return;
}
doResolve({ ok: true, value: JSON.parse(raw) as unknown });
} catch (err) {
doResolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
}
});
req.on("error", (err) => {
doResolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
});
});
}
export function registerGoogleChatWebhookTarget(target: WebhookTarget): () => void {
const key = normalizeWebhookPath(target.path);
const normalizedTarget = { ...target, path: key };
@@ -178,10 +143,19 @@ export async function handleGoogleChatWebhookRequest(
? authHeader.slice("bearer ".length)
: "";
const body = await readJsonBody(req, 1024 * 1024);
const body = await readJsonBodyWithLimit(req, {
maxBytes: 1024 * 1024,
timeoutMs: 30_000,
emptyObjectOnEmpty: false,
});
if (!body.ok) {
res.statusCode = body.error === "payload too large" ? 413 : 400;
res.end(body.error ?? "invalid payload");
res.statusCode =
body.code === "PAYLOAD_TOO_LARGE" ? 413 : body.code === "REQUEST_BODY_TIMEOUT" ? 408 : 400;
res.end(
body.code === "REQUEST_BODY_TIMEOUT"
? requestBodyErrorToText("REQUEST_BODY_TIMEOUT")
: body.error,
);
return true;
}