Build1 publisher2 min readPublished
The cheapest gate runs first so a flood stops before the Supabase auth call
Every route in CogniPrep passes through five gates whose order is set by what each one costs to fail. The sharper detail in the writeup is the header parsing: the standard x-forwarded-for snippet lets any caller pick its own rate-limit key.
The Engineer · Build desk

What happened
- Every API route in CogniPrep is exported through one withApiHandler wrapper that runs five gates in a fixed order, configured per route with options such as a 'write' rate limit preset.
- The first gate is a global limit of 100 requests per minute per IP, placed above the Supabase auth round trip because anything above it is work an unauthenticated caller can force the server to do.
- The post's author writes that the common x-forwarded-for[0] snippet lets a caller rotate its own rate-limit identity, and that their login and password reset actions used to do exactly that.
- When no trustworthy IP header is present, the code hashes coarse request attributes with sha256 and uses the first 16 hex characters as an anon: key instead of bucketing callers under a constant.
- The auth gate calls requireAuth inside the route under a comment citing CVE-2025-29927, the patched Next.js bypass in which a crafted x-middleware-subrequest header skipped middleware.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The ordering caps what may be added at the top of the chain. Logging, tracing or a flag fetch inserted above the IP limiter runs on every unauthenticated request, including the ones you intend to reject.
- exposure When identification degrades, the people throttled are your own users. A single shared bucket turns an edge misconfiguration into an availability failure that arrives without any attacker involved.
- decision Anyone lifting this identifier code has to establish which hop their own edge guarantees appends to x-forwarded-for before the rightmost entry is the right one to trust.
- precedent A patched vulnerability is being kept as a standing design rule here, with auth repeated in every route even though middleware already checks, on the grounds that a header-triggered bypass can recur.
The rightmost-IP rule holds for one deployment shape, and the code comment names it. getClientIdentifier reads x-real-ip first, described there as set by Vercel or another trusted proxy and not spoofable by a client [6]. Only if that header is empty does the function split x-forwarded-for on commas and take the last entry [7]. Rightmost is the client address when a trusted proxy is the last thing that appended to the header. With no proxy in front, the value is whatever the caller typed.
Keying on IP is also what makes the no-IP case dangerous. Pool the unidentifiable callers under one constant and a single client can spend the limit for all of them [10]. On a deploy where the proxy headers are always missing, the entire user base sits inside one 100-per-minute allowance [11]. That is one request every 0.6 seconds, shared [26]. "It is a degraded mode, not a security boundary," the author wrote of the hashed fallback [13].
Ordering by cost shows up most clearly at the CSRF gate. verifyCsrfToken parses the origin header, compares its host to the host header, falls back to referer, and returns false when neither is present [16][19]. It does no I/O, touches no database, and reads nothing about the user [17]. A cross-origin POST is rejected with a 403 before the request costs a single Supabase auth round trip [15][27]. The post puts the principle as cheap and deterministic checks belonging above expensive ones [18].
The auth gate is duplicated work on purpose. If middleware were the only gate, the whole authenticated surface stays open for as long as the bypass works [22]. So every route checks again: "Middleware in this app is a performance and UX gate, and every route behind it also checks for itself," the author wrote [24].
The line worth copying is const limitSubject = user?.id ?? ip [25]. Nothing better than an IP existed that early in the chain [5]. Once requireAuth returns a user, the per-route limit can be charged to an account instead. The published text breaks off after that line [28].
What to watch
- Whether the remaining gates get published, including how the per-route preset keys on the user id after auth.
- A deployment note on which hop appends x-forwarded-for, since the rightmost-entry rule depends on that guarantee.
- A further Next.js middleware bypass advisory would test the choice to repeat the auth check inside every route.