CVE-2026-63421
This vulnerability in Keystone's GraphQL API allows a negative `take` parameter to return more records than intended, bypassing the `maxTake` guard that administrators configured to prevent resource exhaustion. The core failure is straightforward: the validation logic compares the signed integer directly against the magnitude bound rather than checking its absolute value. A `take: -1000` returns one thousand records while the system believes it's enforcing a much smaller limit. The practical impact is denial-of-service at scale. Combined with relationship queries that can amplify the record count, a single unauthenticated GraphQL request can exhaust database connections and degrade co-located services. The unauthenticated attack surface makes this significantly more severe than similar bugs in authenticated contexts—exploitation requires no credentials, no compromised accounts, and no secondary vulnerability. What makes this analytically significant is its recurrence. The same pattern—negative value bypassing magnitude checks—has appeared across Apollo Server, PostGraphile, Hasura, and now Keystone. Each instance is "just an implementation error," but the pattern persists because the GraphQL ecosystem has no guardrail preventing it. Schema authors define magnitude-bounded fields like `take` and `limit` as the generic `Int` scalar (which includes negatives) rather than constrained custom scalars, then validation logic inherits this ambiguity. For defenders: audit your GraphQL resolvers for magnitude-bounded fields. If they accept `Int` directly, verify that validation checks absolute magnitude or that the schema uses a non-negative custom scalar. The fix here is trivial—compare `Math.abs(take)` against `maxTake`—but the systemic gap is that no convention or tooling makes this class of error structurally impossible. Consider whether your team has an "assumption auditing" practice when adding magnitude guards: document what inputs you expect, what the GraphQL layer handles, and what semantic constraints the resolver assumes. The bug that bypassed `maxTake` didn't emerge from forgotten code—it emerged from forgotten reasoning.
Reviewed through automated stages and approved by a human before publication.