fix(proxy): enforce body size limit on actual byteLength, not just Content-Length header

Chunked requests omit Content-Length entirely. The previous guard
only checked the header and was bypassed. Now the body is buffered
first and its byteLength is checked, catching both cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-22 18:17:00 +02:00
committed by marcel
parent e1ae299326
commit 464b8d35d3
2 changed files with 26 additions and 1 deletions

View File

@@ -35,11 +35,21 @@ async function proxy(event: Parameters<RequestHandler>[0]): Promise<Response> {
const hasBody = !NO_BODY_METHODS.has(event.request.method);
// Early exit: reject well-behaved clients that advertise an oversized body
// before we buffer anything.
const contentLength = event.request.headers.get('Content-Length');
if (contentLength && parseInt(contentLength, 10) > 1_048_576) {
return new Response('Payload Too Large', { status: 413 });
}
// Buffer the body so we can check its actual size. This also catches chunked
// requests that omit Content-Length entirely (parseInt(null) → NaN → passes
// the header check above).
const bodyBuffer = hasBody ? await event.request.arrayBuffer() : undefined;
if (bodyBuffer && bodyBuffer.byteLength > 1_048_576) {
return new Response('Payload Too Large', { status: 413 });
}
// Only forward Content-Type from the browser request — other headers (Cookie,
// Authorization, Accept-Encoding, etc.) must not be forwarded directly.
// Authentication is injected by handleFetch in hooks.server.ts from the
@@ -51,7 +61,7 @@ async function proxy(event: Parameters<RequestHandler>[0]): Promise<Response> {
const response = await event.fetch(backendUrl, {
method: event.request.method,
headers: requestHeaders,
body: hasBody ? await event.request.arrayBuffer() : undefined
body: bodyBuffer
});
// Forward all response headers except hop-by-hop so that Content-Disposition,

View File

@@ -69,6 +69,21 @@ describe('catch-all API proxy — body size limit', () => {
expect(mockFetch).not.toHaveBeenCalled();
});
it('returns 413 when actual body exceeds 1 MB even if Content-Length header is absent or lying', async () => {
const mockFetch = vi.fn();
const event = makeEvent('documents', 'POST', mockFetch);
(event.request as Request) = new Request('http://localhost/api/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': '0' },
body: new Uint8Array(1_048_577) // 1 MB + 1 byte, but Content-Length says 0
});
const response = await POST(event as never);
expect(response.status).toBe(413);
expect(mockFetch).not.toHaveBeenCalled();
});
it('forwards request when Content-Length header is absent', async () => {
const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
const event = makeEvent('documents', 'POST', mockFetch);