Claude Cursor GitHub Copilot Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vincentchuwaichow-vanguard-frontier-agentic-skills_frontend_edge-cache-data-bleed-review-febe32a.zip · 11 KB
Part of vincentchuwaichow/vanguard-frontier-agentic — 293 skills

Install

skills CLI npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/edge-cache-data-bleed-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
Git 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 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:

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").
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.

No comments yet.

Reviews (0)

No reviews yet.

Related