Pagination on list endpoints
Why it matters
Unpaginated list endpoints are a denial-of-service waiting to happen: as data grows, findMany({}) with no take fetches every record in the table into memory, exhausting database connection pool, server RAM, and response bandwidth simultaneously. This maps directly to CWE-770 (allocation of resources without limits) and OWASP API 2023 API4 (unrestricted resource consumption). A single authenticated request can trigger minutes of database load. Cost-per-request grows linearly with data, making unbounded lists a latent cost bomb in addition to a reliability risk.
Severity rationale
High because even a single unbounded list query can exhaust database memory and connection pool, causing a denial-of-service condition that affects all users.
Remediation
Add offset-based or cursor-based pagination to every list endpoint in app/api/. Accept page and limit query params, enforce a maximum page size, and include pagination metadata in the response:
const page = Number(searchParams.get('page') ?? 1)
const limit = Math.min(Number(searchParams.get('limit') ?? 20), 100)
const skip = (page - 1) * limit
const [items, total] = await Promise.all([
db.post.findMany({ skip, take: limit }),
db.post.count(),
])
return apiSuccess({ items, meta: { page, limit, total } })
For high-volume tables, replace offset pagination with cursor-based pagination (pass the last record's id as a cursor) to avoid offset drift and index scans.
Detection
- ID:
pagination-list-endpoints - Severity:
high - What to look for: Enumerate all API endpoints that return lists or collections (endpoints where the handler queries for multiple records — look for
findMany,select *, array returns,.getAll(),.list(), cursor-based queries). Check whether each list endpoint implements pagination. Acceptable pagination patterns: offset/limit (?page=1&limit=20), cursor-based (?cursor=<id>&limit=20), keyset pagination. Also check: is there a maximum limit enforced to prevent?limit=999999? - Pass criteria: At least 100% of list endpoints (endpoints returning arrays of resources) implement some form of pagination. A hard limit on page size is enforced (even if it's just capping the maximum). Report even on pass: report the count of list endpoints found and pagination method used.
- Fail criteria: One or more list endpoints return the entire dataset with no pagination — handler queries
findMany({})or equivalent with notake/limit/skipapplied. - Skip (N/A) when: No list endpoints exist (all routes operate on single resources). Signal: no
findMany,selectAll,.list(), or array-returning queries found in any route handler. - Detail on fail: Identify which endpoints lack pagination (e.g., "GET /api/posts returns all records with no limit; GET /api/users does the same. findMany() called without take/skip."). Max 500 chars.
- Remediation: Add pagination to every list endpoint in
app/api/route handlers. The simplest approach is offset-based pagination: acceptpageandlimitquery parameters, apply them to the database query, and include pagination metadata in the response. For example in Prisma:findMany({ skip: (page-1)*limit, take: Math.min(limit, 100) }). Return{ data: items, meta: { page, limit, total } }so clients can implement pagination UI. For high-volume datasets, consider cursor-based pagination instead — it's more efficient and avoids offset drift. Always enforce a maximumlimitto prevent clients from requesting unlimited data.
External references
- cwe · CWE-770 — Allocation of Resources Without Limits or Throttling
- iso-25010:2011 · performance-efficiency.resource-utilization — Resource Utilization
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-api-design·automated