mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
fix(security): enforce bounded webhook body handling
This commit is contained in:
38
src/line/monitor.read-body.test.ts
Normal file
38
src/line/monitor.read-body.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readLineWebhookRequestBody } from "./monitor.js";
|
||||
|
||||
function createMockRequest(chunks: string[]): IncomingMessage {
|
||||
const req = new EventEmitter() as IncomingMessage & { destroyed?: boolean; destroy: () => void };
|
||||
req.destroyed = false;
|
||||
req.headers = {};
|
||||
req.destroy = () => {
|
||||
req.destroyed = true;
|
||||
};
|
||||
|
||||
void Promise.resolve().then(() => {
|
||||
for (const chunk of chunks) {
|
||||
req.emit("data", Buffer.from(chunk, "utf-8"));
|
||||
if (req.destroyed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.emit("end");
|
||||
});
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
describe("readLineWebhookRequestBody", () => {
|
||||
it("reads body within limit", async () => {
|
||||
const req = createMockRequest(['{"events":[{"type":"message"}]}']);
|
||||
const body = await readLineWebhookRequestBody(req, 1024);
|
||||
expect(body).toContain('"events"');
|
||||
});
|
||||
|
||||
it("rejects oversized body", async () => {
|
||||
const req = createMockRequest(["x".repeat(2048)]);
|
||||
await expect(readLineWebhookRequestBody(req, 128)).rejects.toThrow("PayloadTooLarge");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,11 @@ import { chunkMarkdownText } from "../auto-reply/chunk.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
|
||||
import { danger, logVerbose } from "../globals.js";
|
||||
import {
|
||||
isRequestBodyLimitError,
|
||||
readRequestBodyWithLimit,
|
||||
requestBodyErrorToText,
|
||||
} from "../infra/http-body.js";
|
||||
import { normalizePluginHttpPath } from "../plugins/http-path.js";
|
||||
import { registerPluginHttpRoute } from "../plugins/http-registry.js";
|
||||
import { deliverLineAutoReply } from "./auto-reply-delivery.js";
|
||||
@@ -46,6 +51,9 @@ export interface LineProviderMonitor {
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
const LINE_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
||||
const LINE_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
||||
|
||||
// Track runtime state in memory (simplified version)
|
||||
const runtimeState = new Map<
|
||||
string,
|
||||
@@ -85,12 +93,13 @@ export function getLineRuntimeState(accountId: string) {
|
||||
return runtimeState.get(`line:${accountId}`);
|
||||
}
|
||||
|
||||
async function readRequestBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (chunk) => chunks.push(chunk));
|
||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
||||
req.on("error", reject);
|
||||
export async function readLineWebhookRequestBody(
|
||||
req: IncomingMessage,
|
||||
maxBytes = LINE_WEBHOOK_MAX_BODY_BYTES,
|
||||
): Promise<string> {
|
||||
return await readRequestBodyWithLimit(req, {
|
||||
maxBytes,
|
||||
timeoutMs: LINE_WEBHOOK_BODY_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -310,7 +319,7 @@ export async function monitorLineProvider(
|
||||
}
|
||||
|
||||
try {
|
||||
const rawBody = await readRequestBody(req);
|
||||
const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_MAX_BODY_BYTES);
|
||||
const signature = req.headers["x-line-signature"];
|
||||
|
||||
// Validate signature
|
||||
@@ -346,6 +355,18 @@ export async function monitorLineProvider(
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
||||
res.statusCode = 413;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ error: "Payload too large" }));
|
||||
return;
|
||||
}
|
||||
if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
|
||||
res.statusCode = 408;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ error: requestBodyErrorToText("REQUEST_BODY_TIMEOUT") }));
|
||||
return;
|
||||
}
|
||||
runtime.error?.(danger(`line webhook error: ${String(err)}`));
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
|
||||
Reference in New Issue
Block a user