Next.js

Prevent draft content from leaking through public caches

Kamruz
Kamruz
Oct 14, 2026
5 min read
Prevent draft content from leaking through public caches

An editor previews an unpublished article. A later anonymous request to the same slug receives its body. The route checks an editor cookie, but both paths call a cached function keyed only by slug. The first privileged read fills the cache, and the anonymous path reuses the result. The bug is not that the page lacks a login button; the cache has erased the distinction between public and privileged data.

This walkthrough assumes Next.js 15 App Router and examples suitable for 15.1.11. Draft Mode is a framework rendering feature, not a complete authorization system. Any endpoint that enables preview access still needs deliberate authentication or a securely verified preview grant and a safe redirect policy.

Map every way to read a draft

List page rendering, metadata, APIs, related articles, search, sitemap, and image generation. A public page can reject a draft while its metadata or social image exposes the title. A search endpoint may use the same unrestricted repository. Protect the data journey rather than only the visible article component.

For each path, write the actor requirement and publication predicate. An anonymous user can read published records. An editor can read a specific draft after authorization. A background publication process may read broader fields for a separate purpose. These are different contracts even when they share the same table or collection.

Ask an assistant to enumerate read call sites with filenames and query filters. Require a distinction between confirmed public exposure and a potential cache-key collision. The review should produce specific paths to test, not merely recommend that drafts be private.

Separate public and preview functions

A public function should enforce publication in its database query and return only public fields. A preview function should require a verified actor or narrow grant and enforce access to the requested article. Naming them separately reduces the chance that a future caller accidentally uses a broad editor read.

Avoid a boolean argument such as includeDrafts accepted directly from query parameters. The browser can request true. Even an internal boolean is easy to pass incorrectly unless authorization is adjacent and explicit. A verified context object can make the requirement visible, but its construction still belongs in trusted server code.

Use private modules and deliberate projections. A preview may display editorial notes to an editor, while the public record should never include those notes. Returning a whole document and hiding fields in JSX does not prevent serialization through other paths. See private fields in populated records.

Keep privileged results out of shared public caches

The smallest safe fix for the described bug is usually a separate uncached privileged read, plus a published-only public cache. Do not attempt to repair the collision by adding a raw session cookie to a long-lived shared key without examining privacy, cardinality, expiry, and revocation.

import 'server-only'

export async function readPreview(slug: string, actor: VerifiedActor) {
await requireArticleEditAccess(actor, slug)
return articleRepository.findPreviewBySlug(slug)
}

export async function readPublic(slug: string) {
return articleRepository.findPublishedPublicView(slug)
}

These are application interfaces, not framework functions. If readPublic is cached, the cache may contain only results satisfying its published predicate. If publication changes, invalidate that entry. If a draft was already stored in a shared cache by the faulty implementation, changing code alone may not remove the cached data; purge the affected entries as part of the repair.

Inspect HTTP caching too. A privileged preview response should not be stored as a reusable public CDN response. Verify real response headers and hosting behavior. Dynamic rendering at the framework layer does not automatically establish every reverse proxy's privacy policy.

Verify preview entry and exit

A preview-enabling endpoint must validate its grant before setting state and redirect only to an allowed local path. Do not accept an arbitrary absolute redirect from the caller. A preview URL can be forwarded or logged, so keep grants narrow and avoid embedding administrative credentials.

In Next.js 15, request APIs such as draftMode() use async access. Use the version-appropriate interface rather than a copied older synchronous example. Enabling Draft Mode changes rendering behavior; it does not establish whether this actor can access every draft in the database. Keep the content authorization check intact after the mode is enabled.

Provide a clear way to leave preview state. Test that an editor returning to public browsing does not continue seeing privileged content unexpectedly. If a preview grant expires, ensure cached client state or an old open page cannot authorize a new privileged server read.

Test alternating identities and request order

Create one published article, one draft, and two editors with different permissions. Request the draft first as an authorized editor, then anonymously, then as an unauthorized editor. Repeat in the opposite order. Cache leaks often depend on which request populated the entry first.

Inspect the body, title, description, social metadata, and API payload. An anonymous missing page with a draft title in metadata is still exposure. Check public lists and sitemap entries as well. Use unique synthetic phrases so a deterministic assertion can identify leaked fields without handling real draft content.

Repeat across different application instances if the deployment shares a persistent cache. Test after permission revocation and after withdrawal of a published article. A public cached result that remains after withdrawal is a related failure even though no preview session was involved. Publication invalidation covers that transition.

Repair with observable evidence

Record the original collision path, affected cache namespace, and tests used to verify isolation. Avoid logging draft bodies, cookies, or preview grants. Useful logs contain a request identifier, authorization outcome, article identifier, and read mode. Redact values that can independently grant access.

An assistant can compare cache wrappers and query predicates, but it should not receive real credentials or confidential draft prose. A synthetic draft exercises the same boundary. Ask it to explain why the public cache cannot contain a privileged result after the change, and verify that explanation by tracing the code and alternating requests.

If evidence suggests real exposure occurred, follow the application's incident process with the responsible owner. The technical repair should still be concrete: isolate reads, purge contaminated entries, and establish regression tests. Do not stop at adding a cautionary comment to the cached function.

Consider narrower preview caching only when needed

An uncached preview can be adequate for a small editorial workflow. If previews become expensive, first measure the actual cost. A private cache can be designed around verified access, resource identity, content revision, permission changes, and expiry, but that is a separate feature with more failure modes.

Public caching remains valuable when its input contract is public-only. The goal is clear ownership of the data allowed in each cache. Authorization tracing provides a broader method for following that rule through the rest of the application.

Sources

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles