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.
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.
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.
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.
ID: ai-slop-cost-bombs.unbounded-operations.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:
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
}