Skip to content

Commit 7f21566

Browse files
committed
[middleware]: add upper bound to cloneBodyStream
1 parent 86e006b commit 7f21566

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,5 +850,6 @@
850850
"849": "Route %s with \\`dynamic = \"error\"\\` couldn't be rendered statically because it used \\`cookies()\\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering",
851851
"850": "metadataBase is not a valid URL: %s",
852852
"851": "Pass either `webpack` or `turbopack`, not both.",
853-
"852": "Only custom servers can pass `webpack`, `turbo`, or `turbopack`."
853+
"852": "Only custom servers can pass `webpack`, `turbo`, or `turbopack`.",
854+
"853": "Request body exceeded %s"
854855
}

packages/next/src/server/body-streams.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { IncomingMessage } from 'http'
22
import type { Readable } from 'stream'
33
import { PassThrough } from 'stream'
4+
import bytes from 'next/dist/compiled/bytes'
5+
6+
const DEFAULT_BODY_CLONE_SIZE_LIMIT = 50 * 1024 * 1024 // 50MB
47

58
export function requestToBodyStream(
69
context: { ReadableStream: typeof ReadableStream },
@@ -76,13 +79,38 @@ export function getCloneableBody<T extends IncomingMessage>(
7679
const input = buffered ?? readable
7780
const p1 = new PassThrough()
7881
const p2 = new PassThrough()
82+
83+
let bytesRead = 0
84+
const sizeLimit = DEFAULT_BODY_CLONE_SIZE_LIMIT
85+
let limitExceeded = false
86+
7987
input.on('data', (chunk) => {
88+
if (limitExceeded) return
89+
90+
bytesRead += chunk.length
91+
92+
if (bytesRead > sizeLimit) {
93+
limitExceeded = true
94+
const error = new Error(
95+
`Request body exceeded ${bytes.format(sizeLimit)}`
96+
)
97+
p1.destroy(error)
98+
p2.destroy(error)
99+
return
100+
}
101+
80102
p1.push(chunk)
81103
p2.push(chunk)
82104
})
83105
input.on('end', () => {
84-
p1.push(null)
85-
p2.push(null)
106+
if (!limitExceeded) {
107+
p1.push(null)
108+
p2.push(null)
109+
}
110+
})
111+
input.on('error', (err) => {
112+
p1.destroy(err)
113+
p2.destroy(err)
86114
})
87115
buffered = p2
88116
return p1

test/e2e/middleware-fetches-with-body/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ describe('Middleware fetches with body', () => {
3737
res.json({ rawBody, body: req.body })
3838
}
3939
`,
40+
'app/api/test-clone-limit/route.js': `
41+
import { NextResponse } from 'next/server'
42+
43+
export async function POST(request) {
44+
try {
45+
const buffer = await request.arrayBuffer()
46+
return NextResponse.json({
47+
success: true,
48+
bodySize: buffer.byteLength
49+
})
50+
} catch (err) {
51+
return NextResponse.json({
52+
error: err.message
53+
}, { status: 500 })
54+
}
55+
}
56+
`,
4057
'middleware.js': `
4158
import { NextResponse } from 'next/server';
4259
@@ -308,4 +325,46 @@ describe('Middleware fetches with body', () => {
308325
}
309326
}
310327
})
328+
329+
describe('cloneBodyStream size limit', () => {
330+
if (!(global as any).isNextDeploy) {
331+
it('should reject body over 50MB when cloning for middleware', async () => {
332+
const bodySize = 51 * 1024 * 1024
333+
const body = 'Z'.repeat(bodySize)
334+
335+
const res = await fetchViaHTTP(
336+
next.url,
337+
'/api/test-clone-limit',
338+
{},
339+
{
340+
body,
341+
method: 'POST',
342+
}
343+
)
344+
345+
expect(res.status).toBe(400)
346+
})
347+
348+
it('should accept body under 50MB when cloning for middleware', async () => {
349+
const bodySize = 10 * 1024 * 1024
350+
const body = 'Y'.repeat(bodySize)
351+
352+
const res = await fetchViaHTTP(
353+
next.url,
354+
'/api/test-clone-limit',
355+
{},
356+
{
357+
body,
358+
method: 'POST',
359+
}
360+
)
361+
362+
// Should succeed because we're under the 50MB clone limit
363+
expect(res.status).toBe(200)
364+
const data = await res.json()
365+
expect(data.success).toBe(true)
366+
expect(data.bodySize).toBe(bodySize)
367+
})
368+
}
369+
})
311370
})

0 commit comments

Comments
 (0)