QA007mediumPerformance

A per-user query with no limit

What it costs you

This query reads every row a user or tenant owns from a table that only ever grows — messages, events, scans, orders — with no LIMIT, no cursor and no date window. It is fast for a new account and gets slower for every account worth keeping, until the page it feeds stops loading.

The defect, and the fix

Both samples are scanned as app/api/orders/route.ts.

Our test suite runs both through the scanner on every build: the first must be reported, the second must not.

Reported
export async function GET(req) {
  const userId = await requireUser(req);
  const orders = await prisma.order.findMany({ where: { userId } });
  return Response.json({ orders });
}
Not reported
export async function GET(req) {
  const userId = await requireUser(req);
  const orders = await prisma.order.findMany({ where: { userId }, take: 50 });
  return Response.json({ orders });
}

What changed: The query takes a bounded page, so one account with years of history cannot decide how much memory the request uses.

How to fix it

Add a bound the query itself enforces: a LIMIT with a cursor or offset for pages, or a date window for a feed. Bounding it in the caller does not help — the database still reads and ships every row.

Why this rule doesn't cry wolf

Each clause below exists because it was attacked: someone was asked to find correct code that the rule would flag, and the clause is what stopped it. This is published because a check you cannot audit is a check you have to take on faith.

Server files only; an ORM or SQL read, identified from the AST rather than a text window; the WHERE must be scoped to a user/tenant column AND carry no unique-key equality; the table's head noun must be on a closed append-only growth list, so configuration tables read whole are never flagged; no bound token (limit/take/cursor/offset/.range/.slice) and no time-window predicate anywhere in the file; bulk-read paths (export, csv, sitemap, cron, backup, admin) exempt; and suppressed when the rows are immediately reduced by .length/.reduce/count()/Promise.all or projected to ids. Capped at one finding per file and three per scan. The render-side half of the original rule — bounding `.map(` in JSX — was deleted outright, not narrowed.

This is not a security finding

QA007 is reported in its own section, separately from security findings. It does not change your security grade, and it does not fail your build unless you pass --fail-on-quality. The security catalogue lives at /rules.

Check your own code

npx xploitscan scan .

Runs on every plan, including free. All 12 quality checks.