# PDF generation has page count limits

- **Pattern:** `ab-000240` (`ai-slop-cost-bombs.unbounded-operations.pdf-page-limit`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000240
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000240)

## Why it matters

PDF generation is CPU-bound: a renderer like Puppeteer or `@react-pdf/renderer` that loops through a user-supplied `items` array with no length cap is a free compute gift to any attacker. A POST with `items: Array(100000).fill({})` can saturate a server core for minutes, blocking all other requests on that worker. CWE-400 (Uncontrolled Resource Consumption) applies directly: the cost of the operation scales linearly with user input. This is especially acute on serverless platforms where function duration directly maps to billing — one malformed invoice request can burn through your monthly Vercel function budget.

## Severity rationale

Medium because the attack requires a POST body and scales with input length, making it a targeted DoS rather than an accidental one-request outage.

## Remediation

Validate the `items` array length at schema parse time, before the PDF renderer is invoked. Place the limit in `src/lib/invoice.ts` or wherever the Zod schema lives.

```ts
import { z } from 'zod'

const InvoiceSchema = z.object({
  items: z.array(
    z.object({ name: z.string().max(100), price: z.number().min(0) })
  ).min(1).max(100),
})

export async function POST(req: Request) {
  const { items } = InvoiceSchema.parse(await req.json())
  // items is guaranteed ≤ 100 entries here
  const pdf = await generateInvoicePdf(items)
  return new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } })
}
```

Return HTTP 422 (Unprocessable Entity) for schema violations so callers get a meaningful error rather than a timeout.

## Detection

- **ID:** `pdf-page-limit`
- **Severity:** `medium`
- **What to look for:** When a PDF generation library is in `package.json` dependencies (`pdfkit`, `puppeteer`, `playwright`, `@react-pdf/renderer`, `jspdf`, `pdf-lib`, `pdfmake`), count all PDF generation call sites and the source of input (number of items being rendered). For each call site, verify the input array length is validated against a constant (look for `if (items.length > MAX_ROWS)`, `pages.slice(0, MAX_PAGES)`, `z.array(...).max(N)` schema validation, or a hardcoded loop limit).
- **Pass criteria:** 100% of PDF generation call sites validate input length. Report: "X PDF generation call sites, Y validated, 0 unbounded."
- **Fail criteria:** At least 1 PDF generation call processes a user-provided array without length validation.
- **Skip (N/A) when:** No PDF generation library in dependencies.
- **Detail on fail:** `"1 unbounded PDF generation: app/api/invoice/route.ts loops through req.body.items with no length cap — a malicious POST can generate a million-page PDF"`
- **Remediation:** A PDF generator without length limits lets users specify how much CPU your server burns. Validate the input:

  ```ts
  import { z } from 'zod'

  const InvoiceSchema = z.object({
    items: z.array(z.object({ name: z.string(), price: z.number() })).max(100),
  })

  export async function POST(req: Request) {
    const { items } = InvoiceSchema.parse(await req.json())
    // ... generate PDF with at most 100 items
  }
  ```

## External references

- cwe CWE-400
- cwe CWE-770

Taxons: cost-efficiency, injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-000240
