Classify the response before choosing a header

A small business may add a CDN to make its site faster and later discover that the same URL serves different customers. A product photo that is identical for everyone can usually be shared widely. An account page, order detail, invoice PDF, search result filtered by a customer's entitlements, or API response carrying personal data has a different risk. Write a route inventory with method, response type, whether the content changes by user or tenant, and whether any part is sensitive. Do this for successful pages, redirects, error messages and downloads, because a password-reset response or a failed request can contain as much private context as a normal page. The cache policy should follow the response that leaves the origin, not only the URL pattern displayed in a developer's route list.

For a sensitive response that must not be stored by compliant HTTP caches, OWASP recommends `Cache-Control: no-store`. If a personalised but non-sensitive response may be kept by one person's browser while it must never enter a shared cache, `private` expresses that boundary; add revalidation when freshness matters. `no-cache` means a response may be stored but must be revalidated before reuse, so its name must not be read as a storage ban. MDN also warns that the presence of a cookie does not automatically make a response private. The business decision is whether local storage is acceptable for this route, not whether the team can fit every directive into a single long header. Review how a browser back button should behave alongside the confidentiality requirement before selecting the stricter option.

Trace all places a response can be stored

The request can cross a browser HTTP cache, a service worker, a CDN, a reverse proxy and an application cache before reaching the database. Those components do not all obey the same rules. A `no-store` HTTP header controls compliant HTTP caches, but OWASP notes that the browser Cache API used by service workers does not honour HTTP cache headers. Application code must explicitly exclude sensitive responses and remove old entries. A database or Redis cache likewise needs keys that separate the relevant tenant and resource, and must not skip access checks simply because a key hit. Document who owns each layer and which configuration can override the origin. A safe header at the application can be defeated by a CDN rule that force-caches a path or file extension.

Decide which routes are intentionally shareable rather than relying on everything that happens to return HTTP 200. A public help page or versioned image can have a documented shared-cache lifetime. A customer invoice must carry a restrictive policy even if its URL ends in `.pdf`, and a user-specific API response should not become public because it uses a GET request. A cached public page that conditionally inserts a greeting or membership price is no longer identical for all visitors; either remove that private fragment from the shared response or redesign the caching boundary. For data selected by language, encoding or another harmless representation factor, a cache key or `Vary` header may be appropriate. It is not an authorization barrier for a user-specific page.

Test an account page through the full route

Consider a retailer with a CDN in front of its site. `/account/orders` returns the current customer's name and orders after login. The team classifies that response as sensitive and sends `Cache-Control: no-store` from the origin. It then checks the actual response seen through the CDN, not merely an internal server test. One test account loads the page; a second account requests the same URL from a separate browser profile. The second customer must see only their own orders. Staff repeat the check for the JSON endpoint, invoice download, redirect after login and error page. They inspect the response headers and CDN cache status while using test records, avoiding exposure of real customer details in debug logs.

Now suppose the first test discovers an old cached account response. Adding `no-store` to future responses does not erase an object already held by an intermediary; OWASP explicitly calls for purging previously stored entries when a route changes policy. The owner identifies the affected keys or route group, purges them through the cache operator, then retests with both accounts. They also inspect whether a service worker or application cache retained a copy. If an exposure may have occurred, the matter belongs in the business's incident process; a successful new header test does not prove the past was harmless. The release note should record the changed route policy, purge evidence, and the cross-account checks that passed.

Turn the risk into release checks

A useful acceptance matrix has rows for public, personalised and sensitive responses, and columns for origin, CDN, browser and application cache. For each route, capture the response header actually delivered, whether a shared cache stores it, whether an account switch reveals stale content, and whether logout or an authorization change changes what can be read. Test a normal path and a static-looking suffix such as an extra `.css` segment on an authenticated route; OWASP warns that cache deception can arise when a dynamic route is made to look like a file. The expected result is a rejected malformed route, not a sensitive page served under a cacheable-looking name. These are controlled tests with synthetic accounts; they are not claims that a production site is safe simply because a scanner saw a header.

Keep the policy in the delivery workflow. A new CDN rule, a framework upgrade, an API endpoint or a change to how prices are personalised can alter the meaning of a route. Require the owner to classify the new response and rerun the relevant two-account checks before release. Record a way to purge narrowly scoped keys when a mistake is found, rather than relying on a whole-site purge that also disrupts harmless public assets. Review the actual configuration at the edge as well as the source code. This makes cache control a visible product decision: the team chooses where speed is useful, where local storage is acceptable, and where confidentiality requires storage to be avoided.

Include a policy decision for 302 redirects and 404 responses. A redirect after login can carry a destination that reveals account state; an error page can include a customer name or a resource title. Check whether the CDN applies a default TTL to those status codes and whether a framework adds `Cache-Control` only to successful HTML. An API that returns an empty body may still disclose private status through its code or headers. By inventorying these less obvious responses, the team avoids a common blind spot: the main account page is protected while its surrounding workflow is cached under broader rules. Record the intended policy at the response level and inspect the response that reaches an external client.

Test the boundary after a permission change, not only after a fresh login. A staff account may lose access to a client file while an old application cache still holds it; a customer may switch between two business profiles on the same device. The expected behavior is that every request is authorized for the current identity and scope before cached content is returned. Capture enough evidence to know whether a failing response came from origin, edge or client code. Avoid logging the full private page just to debug the cache. If a cache hit bypasses authorization, tightening an HTTP header may not fix the underlying application cache. Repair the access path and repeat the test at the layer where the failure occurred.

For public assets, performance still matters. Keeping an immutable versioned image in a shared cache is compatible with protecting private pages, provided the image is genuinely the same for everyone and its URL changes when the content changes. Do not turn off caching for the whole site because one account route is sensitive; classify precisely and make the public policy explicit too. This also improves incident response: if the business later discovers a private endpoint was marked public, owners can identify the affected route group and purge it without guessing which unrelated assets may be involved. The resulting policy is both safer and easier to maintain because each response has a stated reason for its cache behavior.

Know what a cache policy does not prove

A response header is one control among several. It does not replace authorization on each request, stop a user from taking a screenshot, or remove data that application code deliberately copied elsewhere. It also cannot retroactively prove that no intermediary ever served a response to the wrong account. Keep an inventory of layers, test the route end to end, and monitor changes that can bypass the intended policy. When a customer raises a concern, preserve the minimal operational evidence needed to investigate without copying the sensitive response into more systems. The practical goal is a defensible boundary that can be checked and repaired, not an absolute promise that a header alone prevents every disclosure.

Checks before sign-off

  • Is each personalised route classified, including downloads and errors?
  • Do edge and application caches enforce the chosen policy?
  • Have two synthetic accounts passed the same-URL test?

Sources & further reading