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:
@@ -35,11 +35,21 @@ async function proxy(event: Parameters<RequestHandler>[0]): Promise<Response> {
|
|||||||
|
|
||||||
const hasBody = !NO_BODY_METHODS.has(event.request.method);
|
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');
|
const contentLength = event.request.headers.get('Content-Length');
|
||||||
if (contentLength && parseInt(contentLength, 10) > 1_048_576) {
|
if (contentLength && parseInt(contentLength, 10) > 1_048_576) {
|
||||||
return new Response('Payload Too Large', { status: 413 });
|
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,
|
// Only forward Content-Type from the browser request — other headers (Cookie,
|
||||||
// Authorization, Accept-Encoding, etc.) must not be forwarded directly.
|
// Authorization, Accept-Encoding, etc.) must not be forwarded directly.
|
||||||
// Authentication is injected by handleFetch in hooks.server.ts from the
|
// 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, {
|
const response = await event.fetch(backendUrl, {
|
||||||
method: event.request.method,
|
method: event.request.method,
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
body: hasBody ? await event.request.arrayBuffer() : undefined
|
body: bodyBuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forward all response headers except hop-by-hop so that Content-Disposition,
|
// Forward all response headers except hop-by-hop so that Content-Disposition,
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ describe('catch-all API proxy — body size limit', () => {
|
|||||||
expect(mockFetch).not.toHaveBeenCalled();
|
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 () => {
|
it('forwards request when Content-Length header is absent', async () => {
|
||||||
const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
|
const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
|
||||||
const event = makeEvent('documents', 'POST', mockFetch);
|
const event = makeEvent('documents', 'POST', mockFetch);
|
||||||
|
|||||||
Reference in New Issue
Block a user