VYPR
Moderate severityNVD Advisory· Published Mar 18, 2026· Updated Mar 18, 2026

Next.js: Unbounded postponed resume buffering can lead to DoS

CVE-2026-27979

Description

Next.js is a React framework for building full-stack web applications. Starting in version 16.0.1 and prior to version 16.1.7, a request containing the next-resume: 1 header (corresponding with a PPR resume request) would buffer request bodies without consistently enforcing maxPostponedStateSize in certain setups. The previous mitigation protected minimal-mode deployments, but equivalent non-minimal deployments remained vulnerable to the same unbounded postponed resume-body buffering behavior. In applications using the App Router with Partial Prerendering capability enabled (via experimental.ppr or cacheComponents), an attacker could send oversized next-resume POST payloads that were buffered without consistent size enforcement in non-minimal deployments, causing excessive memory usage and potential denial of service. This is fixed in version 16.1.7 by enforcing size limits across all postponed-body buffering paths and erroring when limits are exceeded. If upgrading is not immediately possible, block requests containing the next-resume header, as this is never valid to be sent from an untrusted client.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nextnpm
>= 16.0.1, < 16.1.716.1.7

Affected products

1

Patches

1
c885d4825f80

ensure maxPostponedStateSize is always respected (#90060)

https://github.com/vercel/next.jsZack TannerFeb 18, 2026via ghsa
7 files changed · +148 68
  • packages/next/src/build/templates/app-page.ts+28 39 modified
    @@ -65,10 +65,12 @@ import type { CacheControl } from '../../server/lib/cache-control'
     import { ENCODED_TAGS } from '../../server/stream-utils/encoded-tags'
     import { sendRenderResult } from '../../server/send-payload'
     import { NoFallbackError } from '../../shared/lib/no-fallback-error.external'
    +import { parseMaxPostponedStateSize } from '../../shared/lib/size-limit'
     import {
    -  DEFAULT_MAX_POSTPONED_STATE_SIZE,
    -  parseMaxPostponedStateSize,
    -} from '../../shared/lib/size-limit'
    +  getMaxPostponedStateSize,
    +  getPostponedStateExceededErrorMessage,
    +  readBodyWithSizeLimit,
    +} from '../../server/lib/postponed-request-body'
     
     // These are injected by the loader afterwards.
     
    @@ -243,23 +245,13 @@ export async function handler(
         typeof resumeStateLengthHeader === 'string'
       ) {
         const stateLength = parseInt(resumeStateLengthHeader, 10)
    -    const maxPostponedStateSize =
    -      nextConfig.experimental.maxPostponedStateSize ??
    -      DEFAULT_MAX_POSTPONED_STATE_SIZE
    -    const maxPostponedStateSizeBytes = parseMaxPostponedStateSize(
    -      nextConfig.experimental.maxPostponedStateSize
    -    )
    +    const { maxPostponedStateSize, maxPostponedStateSizeBytes } =
    +      getMaxPostponedStateSize(nextConfig.experimental.maxPostponedStateSize)
     
         if (!isNaN(stateLength) && stateLength > 0) {
    -      if (
    -        maxPostponedStateSizeBytes === undefined ||
    -        stateLength > maxPostponedStateSizeBytes
    -      ) {
    +      if (stateLength > maxPostponedStateSizeBytes) {
             res.statusCode = 413
    -        res.end(
    -          `Postponed state exceeded ${maxPostponedStateSize} limit. ` +
    -            `To configure the limit, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/max-postponed-state-size`
    -        )
    +        res.end(getPostponedStateExceededErrorMessage(maxPostponedStateSize))
             ctx.waitUntil?.(Promise.resolve())
             return null
           }
    @@ -280,24 +272,16 @@ export async function handler(
               : 1024 * 1024 // 1 MB
           const maxTotalBodySize = stateLength + actionBodySizeLimitBytes
     
    -      // Read the entire body, checking size as we go.
    -      const bodyChunks: Array<Buffer> = []
    -      let size = 0
    -      for await (const chunk of req) {
    -        const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
    -        size += buffer.byteLength
    -        if (size > maxTotalBodySize) {
    -          res.statusCode = 413
    -          res.end(
    -            `Request body exceeded limit. ` +
    -              `To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`
    -          )
    -          ctx.waitUntil?.(Promise.resolve())
    -          return null
    -        }
    -        bodyChunks.push(buffer)
    +      const fullBody = await readBodyWithSizeLimit(req, maxTotalBodySize)
    +      if (fullBody === null) {
    +        res.statusCode = 413
    +        res.end(
    +          `Request body exceeded limit. ` +
    +            `To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`
    +        )
    +        ctx.waitUntil?.(Promise.resolve())
    +        return null
           }
    -      const fullBody = Buffer.concat(bodyChunks)
     
           if (fullBody.length >= stateLength) {
             // Extract postponed state from the beginning
    @@ -323,15 +307,20 @@ export async function handler(
         req.headers[NEXT_RESUME_HEADER] === '1' &&
         req.method === 'POST'
       ) {
    +    const { maxPostponedStateSize, maxPostponedStateSizeBytes } =
    +      getMaxPostponedStateSize(nextConfig.experimental.maxPostponedStateSize)
    +
         // Decode the postponed state from the request body, it will come as
         // an array of buffers, so collect them and then concat them to form
         // the string.
    -
    -    const body: Array<Buffer> = []
    -    for await (const chunk of req) {
    -      body.push(chunk)
    +    const body = await readBodyWithSizeLimit(req, maxPostponedStateSizeBytes)
    +    if (body === null) {
    +      res.statusCode = 413
    +      res.end(getPostponedStateExceededErrorMessage(maxPostponedStateSize))
    +      ctx.waitUntil?.(Promise.resolve())
    +      return null
         }
    -    const postponed = Buffer.concat(body).toString('utf8')
    +    const postponed = body.toString('utf8')
     
         addRequestMeta(req, 'postponed', postponed)
       }
    
  • packages/next/src/server/base-server.ts+22 29 modified
    @@ -6,10 +6,7 @@ import type {
     import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher'
     import type { Params } from './request/params'
     import type { NextConfig, NextConfigRuntime } from './config-shared'
    -import {
    -  DEFAULT_MAX_POSTPONED_STATE_SIZE,
    -  parseMaxPostponedStateSize,
    -} from './config-shared'
    +import { parseMaxPostponedStateSize } from './config-shared'
     import type {
       NextParsedUrlQuery,
       NextUrlWithParsedQuery,
    @@ -155,6 +152,11 @@ import type { PrerenderedRoute } from '../build/static-paths/types'
     import { createOpaqueFallbackRouteParams } from './request/fallback-params'
     import { RouteKind } from './route-kind'
     import type { ErrorModule } from './load-default-error-components'
    +import {
    +  getMaxPostponedStateSize,
    +  getPostponedStateExceededErrorMessage,
    +  readBodyWithSizeLimit,
    +} from './lib/postponed-request-body'
     
     export type FindComponentsResult<
       NextModule extends GenericComponentMod = GenericComponentMod,
    @@ -1069,37 +1071,28 @@ export default abstract class Server<
                 req.headers[NEXT_RESUME_HEADER] === '1' &&
                 req.method === 'POST'
               ) {
    -            // Get the configured max postponed state size.
    -            const maxPostponedStateSize =
    -              this.nextConfig.experimental.maxPostponedStateSize ??
    -              DEFAULT_MAX_POSTPONED_STATE_SIZE
    -            const maxPostponedStateSizeBytes = parseMaxPostponedStateSize(
    -              this.nextConfig.experimental.maxPostponedStateSize
    -            )
    -            if (maxPostponedStateSizeBytes === undefined) {
    -              throw new Error(
    -                'maxPostponedStateSize must be a valid number (bytes) or filesize format string (e.g., "5mb")'
    +            const { maxPostponedStateSize, maxPostponedStateSizeBytes } =
    +              getMaxPostponedStateSize(
    +                this.nextConfig.experimental.maxPostponedStateSize
                   )
    -            }
     
                 // Decode the postponed state from the request body, it will come as
                 // an array of buffers, so collect them and then concat them to form
                 // the string.
    -            const body: Array<Buffer> = []
    -            let size = 0
    -            for await (const chunk of req.body) {
    -              size += Buffer.byteLength(chunk)
    -              if (size > maxPostponedStateSizeBytes) {
    -                res.statusCode = 413
    -                const errorMessage =
    -                  `Postponed state exceeded ${maxPostponedStateSize} limit. ` +
    -                  `To configure the limit, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/max-postponed-state-size`
    -                res.body(errorMessage).send()
    -                return
    -              }
    -              body.push(chunk)
    +            const body = await readBodyWithSizeLimit(
    +              req.body,
    +              maxPostponedStateSizeBytes
    +            )
    +            if (body === null) {
    +              res.statusCode = 413
    +              res
    +                .body(
    +                  getPostponedStateExceededErrorMessage(maxPostponedStateSize)
    +                )
    +                .send()
    +              return
                 }
    -            const postponed = Buffer.concat(body).toString('utf8')
    +            const postponed = body.toString('utf8')
     
                 addRequestMeta(req, 'postponed', postponed)
               }
    
  • packages/next/src/server/lib/postponed-request-body.ts+61 0 added
    @@ -0,0 +1,61 @@
    +import {
    +  DEFAULT_MAX_POSTPONED_STATE_SIZE,
    +  parseMaxPostponedStateSize,
    +} from '../../shared/lib/size-limit'
    +import type { SizeLimit } from '../../types'
    +
    +const INVALID_MAX_POSTPONED_STATE_SIZE_ERROR_MESSAGE =
    +  'maxPostponedStateSize must be a valid number (bytes) or filesize format string (e.g., "5mb")'
    +
    +export type PostponedRequestBodyChunk = Buffer | Uint8Array | string
    +
    +export function getMaxPostponedStateSize(
    +  configuredMaxPostponedStateSize: SizeLimit | undefined
    +): {
    +  maxPostponedStateSize: SizeLimit
    +  maxPostponedStateSizeBytes: number
    +} {
    +  const maxPostponedStateSize =
    +    configuredMaxPostponedStateSize ?? DEFAULT_MAX_POSTPONED_STATE_SIZE
    +  const maxPostponedStateSizeBytes = parseMaxPostponedStateSize(
    +    configuredMaxPostponedStateSize
    +  )
    +
    +  if (maxPostponedStateSizeBytes === undefined) {
    +    throw new Error(INVALID_MAX_POSTPONED_STATE_SIZE_ERROR_MESSAGE)
    +  }
    +
    +  return { maxPostponedStateSize, maxPostponedStateSizeBytes }
    +}
    +
    +export function getPostponedStateExceededErrorMessage(
    +  maxPostponedStateSize: SizeLimit
    +): string {
    +  return (
    +    `Postponed state exceeded ${maxPostponedStateSize} limit. ` +
    +    `To configure the limit, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/max-postponed-state-size`
    +  )
    +}
    +
    +function toBuffer(chunk: PostponedRequestBodyChunk): Buffer {
    +  return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
    +}
    +
    +export async function readBodyWithSizeLimit(
    +  body: AsyncIterable<PostponedRequestBodyChunk>,
    +  maxBodySizeBytes: number
    +): Promise<Buffer | null> {
    +  const chunks: Array<Buffer> = []
    +  let size = 0
    +
    +  for await (const chunk of body) {
    +    const buffer = toBuffer(chunk)
    +    size += buffer.byteLength
    +    if (size > maxBodySizeBytes) {
    +      return null
    +    }
    +    chunks.push(buffer)
    +  }
    +
    +  return Buffer.concat(chunks)
    +}
    
  • test/production/app-dir/max-postponed-state-size/app/layout.tsx+7 0 added
    @@ -0,0 +1,7 @@
    +export default function Layout({ children }: { children: React.ReactNode }) {
    +  return (
    +    <html>
    +      <body>{children}</body>
    +    </html>
    +  )
    +}
    
  • test/production/app-dir/max-postponed-state-size/app/page.tsx+3 0 added
    @@ -0,0 +1,3 @@
    +export default function Page() {
    +  return <p>Hello World</p>
    +}
    
  • test/production/app-dir/max-postponed-state-size/max-postponed-state-size.test.ts+21 0 added
    @@ -0,0 +1,21 @@
    +import { nextTestSetup } from 'e2e-utils'
    +
    +describe('app-dir - max postponed state size', () => {
    +  const { next } = nextTestSetup({
    +    files: __dirname,
    +  })
    +
    +  it('should return 413 when next-resume request exceeds max postponed state size', async () => {
    +    const res = await next.fetch('/', {
    +      method: 'POST',
    +      headers: {
    +        'next-action': 'abc123',
    +        'next-resume': '1',
    +      },
    +      body: Buffer.alloc(1025, 'x'),
    +    })
    +
    +    expect(res.status).toBe(413)
    +    expect(await res.text()).toContain('Postponed state exceeded 1 KB limit.')
    +  })
    +})
    
  • test/production/app-dir/max-postponed-state-size/next.config.js+6 0 added
    @@ -0,0 +1,6 @@
    +module.exports = {
    +  cacheComponents: true,
    +  experimental: {
    +    maxPostponedStateSize: '1 KB',
    +  },
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

1