mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 06:01:23 +00:00
fix(security): enforce bounded webhook body handling
This commit is contained in:
116
src/infra/http-body.test.ts
Normal file
116
src/infra/http-body.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
installRequestBodyLimitGuard,
|
||||
isRequestBodyLimitError,
|
||||
readJsonBodyWithLimit,
|
||||
readRequestBodyWithLimit,
|
||||
} from "./http-body.js";
|
||||
|
||||
function createMockRequest(params: {
|
||||
chunks?: string[];
|
||||
headers?: Record<string, string>;
|
||||
emitEnd?: boolean;
|
||||
}): IncomingMessage {
|
||||
const req = new EventEmitter() as IncomingMessage & { destroyed?: boolean; destroy: () => void };
|
||||
req.destroyed = false;
|
||||
req.headers = params.headers ?? {};
|
||||
req.destroy = () => {
|
||||
req.destroyed = true;
|
||||
};
|
||||
|
||||
if (params.chunks) {
|
||||
void Promise.resolve().then(() => {
|
||||
for (const chunk of params.chunks ?? []) {
|
||||
req.emit("data", Buffer.from(chunk, "utf-8"));
|
||||
if (req.destroyed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (params.emitEnd !== false) {
|
||||
req.emit("end");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
function createMockResponse(): ServerResponse & { body?: string } {
|
||||
const headers: Record<string, string> = {};
|
||||
const res = {
|
||||
headersSent: false,
|
||||
statusCode: 200,
|
||||
setHeader: (key: string, value: string) => {
|
||||
headers[key.toLowerCase()] = value;
|
||||
return res;
|
||||
},
|
||||
end: (body?: string) => {
|
||||
res.headersSent = true;
|
||||
res.body = body;
|
||||
return res;
|
||||
},
|
||||
} as unknown as ServerResponse & { body?: string };
|
||||
return res;
|
||||
}
|
||||
|
||||
describe("http body limits", () => {
|
||||
it("reads body within max bytes", async () => {
|
||||
const req = createMockRequest({ chunks: ['{"ok":true}'] });
|
||||
await expect(readRequestBodyWithLimit(req, { maxBytes: 1024 })).resolves.toBe('{"ok":true}');
|
||||
});
|
||||
|
||||
it("rejects oversized body", async () => {
|
||||
const req = createMockRequest({ chunks: ["x".repeat(512)] });
|
||||
await expect(readRequestBodyWithLimit(req, { maxBytes: 64 })).rejects.toMatchObject({
|
||||
message: "PayloadTooLarge",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns json parse error when body is invalid", async () => {
|
||||
const req = createMockRequest({ chunks: ["{bad json"] });
|
||||
const result = await readJsonBodyWithLimit(req, { maxBytes: 1024, emptyObjectOnEmpty: false });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe("INVALID_JSON");
|
||||
}
|
||||
});
|
||||
|
||||
it("returns payload-too-large for json body", async () => {
|
||||
const req = createMockRequest({ chunks: ["x".repeat(1024)] });
|
||||
const result = await readJsonBodyWithLimit(req, { maxBytes: 10 });
|
||||
expect(result).toEqual({ ok: false, code: "PAYLOAD_TOO_LARGE", error: "Payload too large" });
|
||||
});
|
||||
|
||||
it("guard rejects oversized declared content-length", () => {
|
||||
const req = createMockRequest({
|
||||
headers: { "content-length": "9999" },
|
||||
emitEnd: false,
|
||||
});
|
||||
const res = createMockResponse();
|
||||
const guard = installRequestBodyLimitGuard(req, res, { maxBytes: 128 });
|
||||
expect(guard.isTripped()).toBe(true);
|
||||
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
|
||||
expect(res.statusCode).toBe(413);
|
||||
});
|
||||
|
||||
it("guard rejects streamed oversized body", async () => {
|
||||
const req = createMockRequest({ chunks: ["small", "x".repeat(256)], emitEnd: false });
|
||||
const res = createMockResponse();
|
||||
const guard = installRequestBodyLimitGuard(req, res, { maxBytes: 128, responseFormat: "text" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(guard.isTripped()).toBe(true);
|
||||
expect(guard.code()).toBe("PAYLOAD_TOO_LARGE");
|
||||
expect(res.statusCode).toBe(413);
|
||||
expect(res.body).toBe("Payload too large");
|
||||
});
|
||||
|
||||
it("timeout surfaces typed error", async () => {
|
||||
const req = createMockRequest({ emitEnd: false });
|
||||
const promise = readRequestBodyWithLimit(req, { maxBytes: 128, timeoutMs: 10 });
|
||||
await expect(promise).rejects.toSatisfy((error: unknown) =>
|
||||
isRequestBodyLimitError(error, "REQUEST_BODY_TIMEOUT"),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user