edge-cache-data-bleed-review
Statically review Next.js App Router caching surfaces -- route-level revalidate exports, cache-boundary directives on server functions reading cookies(), generateStaticParams on personalized routes, and Cache-Control/Vary response headers -- for defects that let one user's authen
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/edge-cache-data-bleed-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Edge Cache Data-Bleed Review
Purpose
Review Next.js App Router pages, server functions, Route Handlers, and response headers for the concrete caching-layer defect this skill is scoped to: a per-user, session-derived response getting written into a cache shared across requests (ISR, 'use cache', or a CDN/proxy edge cache) and then replayed to a different user. This skill exists so the review stays anchored to the documented caching primitives Next.js exposes for exactly this problem -- revalidate, 'use cache: private', dynamic = 'force-dynamic', and the Cache-Control/Vary response headers -- instead of drifting into a general "caching performance review" of ISR tuning, fetch cache options, or CDN cost optimization with no data-exposure angle.
When to use
Use this skill when the user asks to:
- review a page or layout under
app/that readscookies()(or another per-request/per-user API) and also carries a route-levelrevalidateexport, - assess whether a server function or Server Component that reads
cookies()needs'use cache: private', - review a dynamic route using
generateStaticParamswhere the generated params are user or account IDs, to check whether the route is safely dynamic or is silently serving a shared, revalidate-windowed cache to authenticated users, - audit a Route Handler's or
getServerSideProps's response headers (Cache-Control,Vary) for a page or API response that includes session-, cookie-, or account-derived data, - perform a pre-launch security review of a Next.js app's caching configuration for cross-user data bleed.
Do not use this skill for:
- ISR/
fetch-cache performance tuning,cacheLife/stale-while-revalidatetiming choices, or CDN cost/latency optimization with no user-specific-data angle -- those are performance concerns, not this skill's data-exposure scope, - a purely client-side
localStorage/sessionStorage/in-memory cache with no server-side or CDN-shared cache layer -- browser-local storage is isolated per browser profile and is out of scope for this skill's cross-user cache-bleed concern, - a bug that requires live traffic reproduction (actually observing User B receive User A's cached response from a deployed CDN) to prove exploitation -- static analysis proves the structural risk, not that it has already been exploited in production.
Context7 Documentation Protocol
- Resolve the Next.js library ID with
resolve-library-id(matched result:/vercel/next.js) before citing anyrevalidate,'use cache: private',generateStaticParams,dynamicroute-segment, or header-caching behavior claim. /vercel/next.jsand/websites/nextjsare high-reputation sources covering the App Router's caching directives ('use cache','use cache: private'), route segment config (revalidate,dynamic), and header-based caching (Cache-Controlin Route Handlers andgetServerSideProps) directly from the framework's own docs and source. Usequery-docsagainst them to confirm exact directive syntax and documented per-user-vs-shared cache semantics before writing a finding.- The
Varyheader's role in CDN/proxy cache-key selection is general HTTP caching semantics, not a Next.js-specific API -- Context7 against/vercel/next.jswill not reliably surface it. Ground aVaryfinding against theofficial_docsMDN/HTTP-standard URL in this skill'smetadata.jsoninstead and label the claimdocumentation-based. - Read
package.jsonfirst to confirm the Next.js major version and whether the app uses the App Router (where'use cache: private',revalidate, anddynamicroute segment config apply) or the Pages Router (getServerSideProps/getStaticProps, which use a different, older caching model) -- do not apply App Router directive syntax to a Pages Router codebase or vice versa. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based, unverified against current release.
Lean operating rules
- All findings in this skill's scope default to HIGH severity, except an incomplete
Varyheader alone (no other caching misconfiguration present), which defaults to MEDIUM-to-HIGH depending on reachability. This is a security-scoped skill: do not downgrade a structural cross-user cache-bleed risk to MEDIUM just because it has not been observed exploited yet -- the risk is in the caching structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this route might leak data between users" without showing the specific
revalidateexport, the specificcookies()read it coexists with, or the specific response header value is not a valid finding -- it is a guess. - Flag any route or layout that combines a route-level
export const revalidate = Nwith acookies()read (directly, or through a server function it calls) and has no'use cache: private'isolating that per-user lookup.revalidategoverns a cache entry shared by every request that hits the route within the window; a personalizedcookies()-derived render sharing that entry is the core defect this skill exists to catch. - Flag any async function that reads
cookies()(or another per-request runtime API) to produce a per-user result and does not declare'use cache: private'as its own cache boundary. Do not accept "there's norevalidateexport nearby so it's probably fine" -- a future refactor that wraps the call site in any shared cache scope re-introduces the bleed silently; the safe pattern is the function declaring its own privacy boundary, not the absence of a nearby cache export. - Flag a dynamic route whose
generateStaticParamsenumerates user- or account-scoped IDs (e.g.userId,accountId,orgId) when the route also carries arevalidateexport and the page renders authenticated, per-user data. The safe idiom isexport const dynamic = 'force-dynamic'on that route with nogenerateStaticParams/revalidatepairing for the authenticated path. - Flag any
Response/NextResponse(Route Handler) orgetServerSidePropsres.setHeadercall that setsCache-Control: public(or omitsCache-Controlwhile sitting behind a CDN that defaults to caching) on a response whose body is derived fromcookies(), a session token, or other per-user request context. The fix isCache-Control: private, or omitting an explicit header and relying on Next.js's own dynamic-rendering default (private, no-cache, no-store, max-age=0, must-revalidate). - Flag any response that sets an explicit
Cache-Control: public(or otherwise CDN-cacheable) header on user-specific data and also sets aVaryheader that does not includeCookie(or whatever header actually carries the session identifier). A CDN keys its cache only on the headers named inVary; withoutCookiepresent, requests from two different users are treated as cache-equivalent. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract -- use for the step-by-step review procedure, the per-defect decision tree, and the required output shape.
- Caching directives and route segment config -- load when reviewing
revalidate,'use cache: private',generateStaticParams, ordynamicroute segment config. - Cache-Control and Vary response headers -- load when reviewing a Route Handler's or
getServerSideProps's response headers. Includes the MDN HTTP-caching grounding reference forVary; load that citation only when aVaryfinding is actually present.
Response minimum
Return, at minimum:
- the route(s), server function(s), and/or response-header call sites in scope,
- ranked findings with file:line evidence, defect category (
revalidate-cookie-bleed,missing-private-cache-boundary,static-params-auth-bleed, orheader-cache-bleed), the concrete data-flow trace (therevalidate/generateStaticParams/dynamicconfig and thecookies()read it coexists with, or the header value and the per-user data it exposes), and a fix sketch matching Next.js's documented pattern, - for every finding involving a
cookies()-derived value, an explicit statement of whether'use cache: private'(or an equivalent per-user isolation boundary) is present on the traced path -- never approve on the assumption one exists elsewhere, - evidence level per finding (
repo evidence,documentation-based, orinference), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover (e.g., "confirming an actual cross-user response replay requires a live CDN reproduction with two concurrent sessions, not static review").
Files (vanguard-frontier-agentic)
-
references
-
cache-control-and-vary-headers.md 3.9 KB
# Cache-Control and Vary Response Headers Load this reference when reviewing a Route Handler's (`route.ts`) or `getServerSideProps`'s response headers for a response derived from per-user data. Includes the MDN grounding reference for `Vary`; cite it only when a `Vary` finding is actually present. ## `Cache-Control: public` on per-user data Any Route Handler or `getServerSideProps` call that sets `Cache-Control: public` (or a directive implying public cacheability, like a bare `s-maxage` with no `private`) authorizes every shared cache between the origin and the client -- CDN edge nodes, corporate proxies, ISP caches -- to store the response body and replay it to a *different* client that requests the same URL. If that body is derived from `cookies()`, a session token, or any other per-user request context, this is a direct cross-user data-exposure path, not a cosmetic caching choice. ```ts // DANGEROUS: CDN may store and replay this session-derived body to any // subsequent requester of the same URL. export async function GET() { const session = (await cookies()).get('session')?.value const userData = await db.users.findBySession(session) return new Response(JSON.stringify(userData), { headers: { 'Cache-Control': 'public, max-age=300' }, }) } ``` The documented fix is `Cache-Control: private`, which tells shared caches not to store the response at all -- only the requesting user's own browser may cache it locally. Next.js's own dynamically-rendered pages (including Draft Mode) set `private, no-cache, no-store, max-age=0, must-revalidate` by default for exactly this reason; an explicit `public` override on a per-user response works against that default protection. ```ts // SAFE: shared caches must not store this response. return new Response(JSON.stringify(userData), { headers: { 'Cache-Control': 'private, max-age=300' }, }) ``` ## `Vary` and cache-key selection `Vary` tells a shared cache which request headers it must fold into the cache key -- two requests that differ only in a header *not* listed in `Vary` are treated as cache-equivalent and may share a cached response. If a response is `Cache-Control: public` (or otherwise CDN-cacheable) and is derived from a session cookie, but `Vary` does not include `Cookie` (or whatever header actually carries the session identifier), the CDN has no signal that two different users' requests need separate cache entries -- it may serve User A's cached response to User B simply because every other varied header (e.g. `Accept-Encoding`) happened to match. ```ts // DANGEROUS: Cookie is absent from Vary, so the CDN cache-keys only on // Accept-Encoding and may conflate two different users' requests. return new Response(JSON.stringify(userData), { headers: { 'Cache-Control': 'public, max-age=300', Vary: 'Accept-Encoding', }, }) ``` ```ts // SAFE: Cookie is included, so the CDN keys its cache on the session value. return new Response(JSON.stringify(userData), { headers: { 'Cache-Control': 'public, max-age=300', Vary: 'Cookie, Accept-Encoding', }, }) ``` This is standard HTTP caching semantics (see MDN's `Vary` reference in `official_docs`), not a Next.js-specific API -- ground this specific claim against the MDN URL rather than Context7 against `/vercel/next.js`, which does not reliably document general `Vary` mechanics. ## Reading the trace correctly A `Vary` gap only matters when the response is otherwise cacheable by a shared cache (`Cache-Control: public`, or a `s-maxage`/CDN-level caching directive). If the response already sets `Cache-Control: private` (or relies on Next.js's dynamic-rendering default), the `Vary` value is moot -- do not raise a `Vary`-only finding on a response that is never stored by a shared cache in the first place. When both are wrong (public `Cache-Control` and an incomplete `Vary`), report them together as one `header-cache-bleed` finding with both header values named, not as two separate findings for the same response. -
caching-directives-and-route-config.md 4.8 KB
# Caching Directives and Route Segment Config Load this reference when reviewing `revalidate`, `'use cache: private'`, `generateStaticParams`, or `dynamic` route segment config. ## `revalidate` and per-user data `export const revalidate = N` (a route segment config export) tells Next.js to treat the rendered output of that route as fresh for up to `N` seconds, then re-render and re-cache it on the next request after the window elapses. This is a single, shared cache entry keyed on the route path (and any static params) -- it is not scoped to an individual visitor. If the render reads `cookies()`, `headers()`, or any other per-request/per-user API, the rendered HTML captured in that cache entry belongs to whichever request happened to trigger the re-render, and every other visitor who hits the same URL within the window receives that same cached, personalized output. ```tsx // DANGEROUS: shared 60-second cache entry serves one user's session data to // every visitor who hits this route within the window. export const revalidate = 60 export default async function DashboardPage() { const session = (await cookies()).get('session')?.value const user = await db.users.findBySession(session) return <Dashboard user={user} /> } ``` The documented fix is not to shorten the window -- it is to remove the route-level `revalidate` export for personalized content and isolate the per-user lookup behind `'use cache: private'` instead (see below), or to make the route fully dynamic. ## `'use cache: private'` `'use cache: private'` is a directive placed as the first statement inside a function body. It allows the function to read runtime request APIs (`cookies()`, `headers()`, `searchParams`) from within a cached scope, but the result is **never stored on the server** -- it is cached only in the requesting user's own browser, making it per-user by definition and safe to combine with cookie-derived lookups. ```tsx async function getUser() { 'use cache: private' const session = (await cookies()).get('session')?.value return db.users.findBySession(session) } ``` Two structural checks matter here, not just the directive's presence: - The directive must be the function's own first statement -- a function that reads `cookies()` and calls out to *another* function that has the directive does not itself get the protection unless the `cookies()` read happens inside the directive-scoped function. - A function with `'use cache: private'` still needs the actual per-request read (`cookies()`, `headers()`) to happen inside it for the isolation to be meaningful -- a function that has the directive but reads no per-request data isn't wrong, but it also isn't evidence that some *other*, undirected function elsewhere is safe. ## `generateStaticParams` and personalized routes `generateStaticParams` pre-computes which dynamic-segment values (`[id]`, `[userId]`, etc.) Next.js should statically render for a route. When the enumerated values are user- or account-scoped IDs and the route also renders authenticated, per-user data, pairing this with `revalidate` reproduces the same shared-cache-entry problem as above, except now the cache key includes the user ID segment -- so specifically, requests for `/account/123` from *different* people (e.g. a shared/public device, or a URL passed between users) hitting the route within the same revalidate window get the same cached response for that ID, not a fresh per-request one. ```tsx // DANGEROUS: authenticated per-user page pre-rendered by ID, then reused for // 60 seconds regardless of who requests it next. export const revalidate = 60 export async function generateStaticParams() { const users = await db.users.findAll() return users.map((u) => ({ userId: u.id })) } ``` The documented alternative for a route whose content must always reflect the current requester's own authenticated view is `export const dynamic = 'force-dynamic'`, which forces the route to render fresh on every request and skip both the static pre-render and the ISR cache entirely. Do not pair `generateStaticParams` enumerating auth-scoped IDs with `revalidate` on a route that renders session-derived content -- if the enumerated IDs are genuinely public and non-personalized (e.g. public blog post slugs), this pairing is fine and not a finding. ## Reading the trace correctly When flagging any of the above, name the exact per-request API call (`cookies()`, `headers()`) and its exact reachability path from the flagged route-segment-config export -- a route segment config alone, with no per-request read anywhere in its render tree, is not a finding; conversely, a per-request read with no route-segment-config export at all is still a `missing-private-cache-boundary` finding on its own (see the main workflow reference), because a future caller could wrap it in a shared cache scope. -
workflow-and-output.md 7.1 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the other two references only for the specific caching surface the code under review actually raises. ## Prerequisites - Confirm the Next.js major version and router in `package.json`. `'use cache: private'`, `revalidate` route segment config, and `dynamic = 'force-dynamic'` are App Router (Next.js 13+ `app/`) concepts. A Pages Router app (`pages/`) uses `getServerSideProps`/`getStaticProps` and `res.setHeader('Cache-Control', ...)` instead -- the header-based findings still apply there, but the directive-based findings (`'use cache: private'`, route segment `revalidate`) do not. - Identify every route, layout, server function, and Route Handler that reads `cookies()`, `headers()`, or another per-request/per-user runtime API. These are the candidate personalization points this skill traces forward from. ## Workflow 1. **Locate every route segment config export.** Grep for `export const revalidate`, `export const dynamic`, and `generateStaticParams` across `app/**/page.tsx`, `app/**/layout.tsx`, and `app/**/route.ts`. 2. **For each route carrying `revalidate`, trace whether it (or any server function/component it renders) reads `cookies()`, `headers()`, or another per-request API.** If it does, check whether that specific lookup is isolated behind `'use cache: private'`. See `references/caching-directives-and-route-config.md` for the decision tree. 3. **For each route with `generateStaticParams`, inspect the params being enumerated.** If they are user-, account-, or org-scoped IDs, check whether the route is also authenticated (renders session-derived data) and whether it carries `revalidate` or defaults to static generation without `dynamic = 'force-dynamic'`. 4. **Enumerate every server function (not just page-level components) that reads `cookies()`/`headers()` to produce a per-user result.** For each, confirm `'use cache: private'` is declared as the function's own first statement -- do not accept "no cache export nearby" as sufficient; the isolation must be intrinsic to the function. 5. **Enumerate every Route Handler (`route.ts`) and `getServerSideProps` call that sets response headers.** For each, trace whether the response body is derived from `cookies()`/session/account data, and check the `Cache-Control` and `Vary` header values. See `references/cache-control-and-vary-headers.md`. 6. **Produce ranked findings** using the output contract below. ## Decision tree - Route carries `export const revalidate = N` and reads `cookies()` (directly or via a called function) with no `'use cache: private'` isolating that read → **HIGH** finding, `revalidate-cookie-bleed`. The shared ISR cache entry serves the first requester's personalized render to everyone else within the window. - A server function reads `cookies()`/`headers()` to produce a per-user result and declares no `'use cache: private'` (or equivalent) boundary of its own → **HIGH** finding, `missing-private-cache-boundary`, regardless of whether a `revalidate` export currently sits nearby -- the risk is that any future caching wrapper reintroduces the bleed with no defense at the function itself. - `generateStaticParams` enumerates user/account-scoped IDs, the route also carries `revalidate`, and the page renders authenticated per-user data → **HIGH** finding, `static-params-auth-bleed`. The safe fix is `export const dynamic = 'force-dynamic'` with no `revalidate`/static-params pairing for that authenticated path. - `generateStaticParams` enumerates IDs for genuinely public, non-personalized content (e.g. public blog post IDs with no auth-gated data in the render) → not a finding; state this explicitly rather than silently omitting it. - A Route Handler or `getServerSideProps` response derived from `cookies()`/session data sets `Cache-Control: public` (or omits `Cache-Control` while behind a CDN that caches by default) → **HIGH** finding, `header-cache-bleed`. - The same response also sets `Vary` but the value does not include `Cookie` (or the actual header carrying the session identifier) → **MEDIUM-to-HIGH** finding depending on whether the surface is public-facing or requires an existing session to reach, `header-cache-bleed`. A `Cache-Control: private` response makes the `Vary` gap moot -- only flag it when the response is otherwise CDN-cacheable. - Response sets `Cache-Control: private`, or relies on Next.js's documented dynamic-rendering default (`private, no-cache, no-store, max-age=0, must-revalidate`), for a `cookies()`-derived response → not a finding. ## Output contract Every response from this skill must return: 1. **Scope** -- the route(s), server function(s), and/or response-header call sites reviewed. 2. **Ranked findings** -- each with file:line, defect category (`revalidate-cookie-bleed` / `missing-private-cache-boundary` / `static-params-auth-bleed` / `header-cache-bleed`), the concrete data-flow trace (the route segment config and the per-request read it coexists with, or the header value and the per-user data source), and a fix sketch matching Next.js's documented pattern. 3. **Cache-boundary status per `cookies()`-derived finding** -- an explicit statement of whether `'use cache: private'` (or `Cache-Control: private`) is present on the traced path; never infer one exists elsewhere in the codebase. 4. **Evidence level per finding** -- `repo evidence`, `documentation-based`, or `inference`. Label structural risk findings as structural risk explicitly -- do not imply confirmed exploitation without live evidence (e.g., a captured cross-user response from a deployed CDN). 5. **Verdict** -- approve / approve-with-notes / block. 6. **Open questions or out-of-scope items** -- e.g., "confirming an actual cross-user response replay requires a live CDN reproduction with two concurrent sessions, not static review," or "ISR revalidation-timing tuning is out of scope -- this review covers cache-boundary correctness, not cache-hit-ratio performance." ## When to push back Push back if the user asks to: - approve a `revalidate`-plus-`cookies()` route because "the window is only 10 seconds, so the exposure is small" -- any nonzero shared window during which a personalized response is replayed to a different user is a data-exposure defect, not a tunable performance tradeoff, - skip the `'use cache: private'` check on a server function because "nothing currently wraps it in a shared cache" -- the isolation needs to be intrinsic to the function so a future refactor cannot silently reintroduce the bleed, - treat a `Vary: Cookie` header as sufficient on its own without checking `Cache-Control` -- if `Cache-Control` is `private` (or absent, defaulting to Next.js's dynamic-rendering default), the CDN never caches the response at all and `Vary` is moot; if `Cache-Control` is `public`, `Vary: Cookie` alone does not make blanket caching safe unless every downstream cache actually honors `Vary` correctly, - downgrade an untraced `generateStaticParams`-plus-`revalidate` finding on an authenticated route to informational because "it's probably fine" -- this skill's default is HIGH for exactly this class of unproven claim.
-
-
metadata.json 1.9 KB
{ "id": "edge-cache-data-bleed-review", "name": "Edge Cache Data-Bleed Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews Next.js App Router caching surfaces -- route-level revalidate exports, 'use cache: private' boundaries on cookies()-reading server functions, generateStaticParams on personalized routes, and Cache-Control/Vary response headers -- for defects that let one user's authenticated response be cached and replayed to a different user, grounding claims via Context7 and Next.js's own caching documentation.", "source_type": "original", "official_docs": [ "https://nextjs.org/docs/app/api-reference/directives/use-cache-private", "https://nextjs.org/docs/app/guides/incremental-static-regeneration", "https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config", "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary", "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" ], "security_notes": "This skill's entire scope is security-critical: a shared cache entry (ISR revalidate window, an uncached-boundary server function, or a CDN/proxy edge cache) serving one user's session-derived response to a different user is a cross-user data-exposure defect, not a performance bug. Every finding in this skill defaults to HIGH severity unless proven otherwise with a concrete 'use cache: private' or Cache-Control: private boundary on the traced path. Static-review-only skill: it reads and greps route files, server functions, and response-header call sites but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/edge-cache-data-bleed-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 9.5 KB
--- name: edge-cache-data-bleed-review description: Statically review Next.js App Router caching surfaces -- route-level revalidate exports, cache-boundary directives on server functions reading cookies(), generateStaticParams on personalized routes, and Cache-Control/Vary response headers -- for defects that let one user's authenticated response be cached and served back to a different user. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # Edge Cache Data-Bleed Review ## Purpose Review Next.js App Router pages, server functions, Route Handlers, and response headers for the concrete caching-layer defect this skill is scoped to: a per-user, session-derived response getting written into a cache shared across requests (ISR, `'use cache'`, or a CDN/proxy edge cache) and then replayed to a different user. This skill exists so the review stays anchored to the documented caching primitives Next.js exposes for exactly this problem -- `revalidate`, `'use cache: private'`, `dynamic = 'force-dynamic'`, and the `Cache-Control`/`Vary` response headers -- instead of drifting into a general "caching performance review" of ISR tuning, `fetch` cache options, or CDN cost optimization with no data-exposure angle. ## When to use Use this skill when the user asks to: - review a page or layout under `app/` that reads `cookies()` (or another per-request/per-user API) and also carries a route-level `revalidate` export, - assess whether a server function or Server Component that reads `cookies()` needs `'use cache: private'`, - review a dynamic route using `generateStaticParams` where the generated params are user or account IDs, to check whether the route is safely dynamic or is silently serving a shared, revalidate-windowed cache to authenticated users, - audit a Route Handler's or `getServerSideProps`'s response headers (`Cache-Control`, `Vary`) for a page or API response that includes session-, cookie-, or account-derived data, - perform a pre-launch security review of a Next.js app's caching configuration for cross-user data bleed. Do not use this skill for: - ISR/`fetch`-cache performance tuning, `cacheLife`/`stale-while-revalidate` timing choices, or CDN cost/latency optimization with no user-specific-data angle -- those are performance concerns, not this skill's data-exposure scope, - a purely client-side `localStorage`/`sessionStorage`/in-memory cache with no server-side or CDN-shared cache layer -- browser-local storage is isolated per browser profile and is out of scope for this skill's cross-user cache-bleed concern, - a bug that requires live traffic reproduction (actually observing User B receive User A's cached response from a deployed CDN) to prove exploitation -- static analysis proves the structural risk, not that it has already been exploited in production. ## Context7 Documentation Protocol - Resolve the Next.js library ID with `resolve-library-id` (matched result: `/vercel/next.js`) before citing any `revalidate`, `'use cache: private'`, `generateStaticParams`, `dynamic` route-segment, or header-caching behavior claim. - `/vercel/next.js` and `/websites/nextjs` are high-reputation sources covering the App Router's caching directives (`'use cache'`, `'use cache: private'`), route segment config (`revalidate`, `dynamic`), and header-based caching (`Cache-Control` in Route Handlers and `getServerSideProps`) directly from the framework's own docs and source. Use `query-docs` against them to confirm exact directive syntax and documented per-user-vs-shared cache semantics before writing a finding. - The `Vary` header's role in CDN/proxy cache-key selection is general HTTP caching semantics, not a Next.js-specific API -- Context7 against `/vercel/next.js` will not reliably surface it. Ground a `Vary` finding against the `official_docs` MDN/HTTP-standard URL in this skill's `metadata.json` instead and label the claim `documentation-based`. - Read `package.json` first to confirm the Next.js major version and whether the app uses the App Router (where `'use cache: private'`, `revalidate`, and `dynamic` route segment config apply) or the Pages Router (`getServerSideProps`/`getStaticProps`, which use a different, older caching model) -- do not apply App Router directive syntax to a Pages Router codebase or vice versa. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`. ## Lean operating rules - All findings in this skill's scope default to HIGH severity, except an incomplete `Vary` header alone (no other caching misconfiguration present), which defaults to MEDIUM-to-HIGH depending on reachability. This is a security-scoped skill: do not downgrade a structural cross-user cache-bleed risk to MEDIUM just because it has not been observed exploited yet -- the risk is in the caching structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this route might leak data between users" without showing the specific `revalidate` export, the specific `cookies()` read it coexists with, or the specific response header value is not a valid finding -- it is a guess. - Flag any route or layout that combines a route-level `export const revalidate = N` with a `cookies()` read (directly, or through a server function it calls) and has no `'use cache: private'` isolating that per-user lookup. `revalidate` governs a cache entry shared by every request that hits the route within the window; a personalized `cookies()`-derived render sharing that entry is the core defect this skill exists to catch. - Flag any async function that reads `cookies()` (or another per-request runtime API) to produce a per-user result and does not declare `'use cache: private'` as its own cache boundary. Do not accept "there's no `revalidate` export nearby so it's probably fine" -- a future refactor that wraps the call site in any shared cache scope re-introduces the bleed silently; the safe pattern is the function declaring its own privacy boundary, not the absence of a nearby cache export. - Flag a dynamic route whose `generateStaticParams` enumerates user- or account-scoped IDs (e.g. `userId`, `accountId`, `orgId`) when the route also carries a `revalidate` export and the page renders authenticated, per-user data. The safe idiom is `export const dynamic = 'force-dynamic'` on that route with no `generateStaticParams`/`revalidate` pairing for the authenticated path. - Flag any `Response`/`NextResponse` (Route Handler) or `getServerSideProps` `res.setHeader` call that sets `Cache-Control: public` (or omits `Cache-Control` while sitting behind a CDN that defaults to caching) on a response whose body is derived from `cookies()`, a session token, or other per-user request context. The fix is `Cache-Control: private`, or omitting an explicit header and relying on Next.js's own dynamic-rendering default (`private, no-cache, no-store, max-age=0, must-revalidate`). - Flag any response that sets an explicit `Cache-Control: public` (or otherwise CDN-cacheable) header on user-specific data and also sets a `Vary` header that does not include `Cookie` (or whatever header actually carries the session identifier). A CDN keys its cache only on the headers named in `Vary`; without `Cookie` present, requests from two different users are treated as cache-equivalent. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) -- use for the step-by-step review procedure, the per-defect decision tree, and the required output shape. - [Caching directives and route segment config](references/caching-directives-and-route-config.md) -- load when reviewing `revalidate`, `'use cache: private'`, `generateStaticParams`, or `dynamic` route segment config. - [Cache-Control and Vary response headers](references/cache-control-and-vary-headers.md) -- load when reviewing a Route Handler's or `getServerSideProps`'s response headers. Includes the MDN HTTP-caching grounding reference for `Vary`; load that citation only when a `Vary` finding is actually present. ## Response minimum Return, at minimum: - the route(s), server function(s), and/or response-header call sites in scope, - ranked findings with file:line evidence, defect category (`revalidate-cookie-bleed`, `missing-private-cache-boundary`, `static-params-auth-bleed`, or `header-cache-bleed`), the concrete data-flow trace (the `revalidate`/`generateStaticParams`/`dynamic` config and the `cookies()` read it coexists with, or the header value and the per-user data it exposes), and a fix sketch matching Next.js's documented pattern, - for every finding involving a `cookies()`-derived value, an explicit statement of whether `'use cache: private'` (or an equivalent per-user isolation boundary) is present on the traced path -- never approve on the assumption one exists elsewhere, - evidence level per finding (`repo evidence`, `documentation-based`, or `inference`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover (e.g., "confirming an actual cross-user response replay requires a live CDN reproduction with two concurrent sessions, not static review").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.