fix(gateway): enforce request body size limits
This commit is contained in:
@@ -220,6 +220,24 @@ describe('WebhookHandler', () => {
|
||||
expect(messages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects oversized payloads with 413', async () => {
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any, 'shared_session', 16);
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
await handler.connect();
|
||||
|
||||
const req = mockRequest('x'.repeat(64));
|
||||
const res = mockResponse();
|
||||
|
||||
const result = await handler.handleRequest('test-hook', req, res);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(res.statusCode_).toBe(413);
|
||||
expect(messages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('forwards response to output channel on send()', async () => {
|
||||
const mockOutputAdapter = {
|
||||
send: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
+19
-12
@@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { WebhookConfig } from '../config/schema.js';
|
||||
import type { ChannelAdapter, ChannelStatus, InboundMessage, OutboundMessage } from '../channels/types.js';
|
||||
import { auditLogger } from '../audit/index.js';
|
||||
import { RequestBodyTooLargeError, readRequestBody } from '../utils/httpBody.js';
|
||||
|
||||
/** Minimal interface for the parts of ChannelRegistry we need. */
|
||||
interface ChannelLookup {
|
||||
@@ -11,16 +12,6 @@ interface ChannelLookup {
|
||||
|
||||
type DeliveryMode = 'shared_session' | 'isolated_job';
|
||||
|
||||
/** Read the full request body as a string. */
|
||||
function readBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** Verify HMAC-SHA256 signature from the X-Webhook-Signature header. */
|
||||
function verifyHmac(body: string, secret: string, signature: string): boolean {
|
||||
const expected = createHmac('sha256', secret).update(body).digest('hex');
|
||||
@@ -62,6 +53,7 @@ function renderTemplate(template: string, body: string): string {
|
||||
}
|
||||
|
||||
export class WebhookHandler implements ChannelAdapter {
|
||||
private static readonly DEFAULT_MAX_REQUEST_BODY_BYTES = 1_048_576; // 1 MiB
|
||||
readonly name = 'webhook';
|
||||
private _status: ChannelStatus = 'disconnected';
|
||||
private messageHandler?: (msg: InboundMessage) => void;
|
||||
@@ -71,6 +63,7 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
private readonly webhookConfigs: WebhookConfig[],
|
||||
private readonly channelLookup: ChannelLookup,
|
||||
private readonly deliveryMode: DeliveryMode = 'shared_session',
|
||||
private readonly maxRequestBodyBytes: number = WebhookHandler.DEFAULT_MAX_REQUEST_BODY_BYTES,
|
||||
) {
|
||||
for (const webhook of webhookConfigs) {
|
||||
this.webhooks.set(webhook.name, webhook);
|
||||
@@ -137,7 +130,21 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = await readBody(req);
|
||||
let body = '';
|
||||
try {
|
||||
body = await readRequestBody(req, { maxBytes: this.maxRequestBodyBytes });
|
||||
} catch (err) {
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
auditLogger?.webhookDenied(webhookName, `Payload too large (>${this.maxRequestBodyBytes} bytes)`);
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
return false;
|
||||
}
|
||||
auditLogger?.webhookDenied(webhookName, err instanceof Error ? err.message : 'Failed to read request body');
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify HMAC if secret is configured
|
||||
const signatureVerified = !webhook.secret;
|
||||
@@ -195,4 +202,4 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
}
|
||||
|
||||
// Export helpers for testing
|
||||
export { readBody as _readBody, verifyHmac as _verifyHmac, renderTemplate as _renderTemplate };
|
||||
export { verifyHmac as _verifyHmac, renderTemplate as _renderTemplate };
|
||||
|
||||
Reference in New Issue
Block a user