November 5, 2026
A Cache Hit Still Needs an Authorization Check
Imagine an analyst opening a report, losing access to its workspace, and opening the same report again. The database permissions are…

By Roman Fedytskyi
5 min read
Imagine an analyst opening a report, losing access to its workspace, and opening the same report again. The database permissions are correct. The application still returns the report because a cached result takes a different path. The optimization worked; the access boundary did not.
🌟 Non-members read here
That hypothetical failure deserves attention whenever a team changes how it caches application data. A request can be fast, technically successful, and wrong about who should receive the result. Reviewing cache performance without reviewing the return path leaves that question unanswered.
Vercel's October 30 announcement brings the issue into a current engineering conversation. The company is consolidating Data Cache into Runtime Cache and says existing Next.js fetch caching and unstable_cache code does not need to change. That is a platform change, not evidence of an authorization vulnerability. It is a useful occasion to inspect the assumptions around the data being reused.
For a sensitive application, start with a concrete question: what must still be checked when the expensive computation does not run?
Separate permission from reusable work
Consider a hypothetical loan portfolio report. Producing its aggregate figures is expensive, and several analysts in the same institution may be entitled to see identical results. Recomputing everything for each person would be an understandable target for optimization.
The tempting implementation wraps the entire report-loading function in a cache. That function also happens to check workspace membership. On a miss, the permission check runs. On a hit, the function body may not execute, so the design must establish where authorization happens instead.
Draw two paths on a whiteboard: cold request and warm request. Mark the point where the current caller's permission is established on each path. If one line reaches the response without passing that point, the diagram has found a specific implementation question worth resolving before release.
Next.js recommends a server-side data access layer that performs authorization and returns minimal data transfer objects. Apply that separation deliberately. Keep the current access decision outside a reusable computation when the cache would otherwise skip it.
A key describes a result, not a caller's rights
Next.js documents that use cache keys incorporate serializable arguments and captured outer-scope values. Read request cookies or headers outside that cached scope and pass the necessary values in. The framework can key a value you supplied; your application must establish what that value means.
For the portfolio example, derive the tenant from verified server-side membership. Treat an incoming workspace identifier as a requested target, then resolve whether the caller may access it. Do not give a query parameter the authority of an authenticated tenant context.
Choose dimensions according to how the result varies. A portfolio revision, currency, and reporting period might change the aggregate. An entitlement class might change the fields returned. A tenant identifier separates institutional data only if the underlying query is scoped to that same tenant.
Avoid adding identity dimensions reflexively. Two authorized people who receive exactly the same aggregate may share the computation while passing separate access checks. The design goal is to make the permitted sharing explicit, not to maximize either cache fragmentation or cache reuse.
Make the return path reviewable
Use a short, application-specific wrapper to show the intended order. This pseudocode omits framework APIs, response handling, and storage details. Its helpers represent contracts the implementation must supply; it is not a production authorization library.
readPortfolioReport(request, portfolioId):
principal = authenticateCurrentRequest(request)
grant = authorizeCurrentPortfolioRead(principal, portfolioId)
report = reusableAggregate(
grant.tenantId,
grant.portfolioId,
grant.reportRevision,
grant.approvedView
)
return selectApprovedFields(report, grant.approvedView)readPortfolioReport(request, portfolioId):
principal = authenticateCurrentRequest(request)
grant = authorizeCurrentPortfolioRead(principal, portfolioId)
report = reusableAggregate(
grant.tenantId,
grant.portfolioId,
grant.reportRevision,
grant.approvedView
)
return selectApprovedFields(report, grant.approvedView)In this design, every application return path first establishes a grant. The reusable operation receives verified scope and queries only that scope. An approved view is an application-owned value describing a permitted result variant, never a free-form label supplied by the browser.
Write the helper contracts beside the implementation. Does authorization verify that the portfolio belongs to the tenant? Does the database query use both identifiers? Can a future caller import the cached helper directly and bypass the wrapper? Answer those questions in code review rather than relying on a suggestive function name.
Place the raw reusable helper behind the data access boundary and expose the checked operation to request handlers. Ask a reviewer to trace one successful read from entry point to returned fields. Repeat for a denied read with an already populated cache.
Choose the revocation promise
Losing access is where a static cache diagram becomes a time problem. For the hypothetical analyst, define what happens after workspace removal has been acknowledged. Should the next new server request fail immediately, or does the product explicitly allow a bounded delay?
Write that expectation as a testable product rule. If the requirement is immediate denial, choose an authorization mechanism whose consistency supports it. A ten-minute cache of membership decisions cannot satisfy that requirement merely because the report cache has an excellent key.
OWASP's Next.js guidance calls for preventing reuse under stale permissions after authorization or tenancy changes. Invalidation and permission versions are possible tools, but they need a defined relationship with the authoritative update. For a strict rule, prefer checking current authorization on each new protected read rather than trusting a cached positive decision without that relationship.
Also distinguish a new read from information already delivered. The server can deny the next request; that does not retrieve a report the browser already received. Describe the promise narrowly enough that the engineering team can implement and demonstrate it.
Warm the cache with the wrong person in mind
Build a small integration scenario around two institutions and several roles. Use synthetic reports with unmistakable markers, such as institution-A-only and reviewer-only. Seed overlapping portfolio identifiers deliberately so the test cannot pass only because every fixture has a globally convenient name.
Have an authorized analyst populate the cache, then request the same target as an outsider. Check both the HTTP outcome and the response body. Repeat as a same-tenant user who lacks report access, then as someone allowed to see the aggregate but not restricted fields.
Next, remove the original analyst's membership and repeat the request against the warm cache. Verify the promised revocation behavior. Exercise the cold path separately so the suite can distinguish a cache-specific defect from a general authorization defect.
Include concurrency if revocation during a read matters to the product. Define the policy for a request authorized before removal but completed afterward. A deterministic test needs an agreed ordering rule; it cannot invent one from the implementation's timing.
Look beyond the data cache
The checked wrapper only helps when the request reaches it. Inventory any response cache, route cache, browser state, or upstream service that can return protected content without invoking that wrapper. A correctly protected data read does not establish that every delivery path is protected.
For each layer, identify its audience and where the access decision occurs. Ask whether a shared response contains only information that its entire audience may receive. Review prefetching, back navigation, logout, and account switching with the same synthetic identities used in the server tests.
Keep the investigation bounded. Pick one report endpoint and follow its complete path before creating a company-wide cache taxonomy. One verified path gives the team an example it can reuse, including places where caching was intentionally excluded.
Measure the safe design
Separate authorization time from aggregate computation time in the canary. Measure warm and cold latency, query volume, entry count, and invalidation behavior. If the access check becomes the dominant cost, optimize that mechanism against its consistency requirement rather than moving it into a cache that silently changes the promise.
Keep raw session tokens and report contents out of the diagnostic record. Log the policy outcome and enough approved context to investigate a denied request. A cache review should not introduce a new data-exposure path through its own debugging output.
Roll out with a fallback that preserves the checked wrapper. Disabling aggregate reuse can be a reasonable emergency response; removing the access check is not an equivalent rollback. Give the on-call engineer a way to tell which configuration is running.
Before expanding the canary, ask someone outside the implementation team to explain the audience and revocation promise from the review notes. Have that person identify the test that demonstrates each promise. If the explanation depends on an undocumented assumption about a framework default, make the assumption explicit and verify it against the deployed version.
The practical deliverable is modest: one expensive read, an explicit audience, and a warm-cache test that proves a denied caller still receives no protected result. Cache infrastructure can evolve underneath that contract. The application remains responsible for deciding who may receive the next response.