SecHead
Scansiona un sitoContattaci
Header Guide14 min read

Cache-Control: Protecting Sensitive Data

Proper caching is great for performance, but dangerous for security. Learn how to stop browsers and proxies from storing sensitive pages.

SL
Seven Labs · 21 June 2026
2,806 words

Cache-Control Security: Defending Against Cache Poisoning and Data Leaks

Quick Answer: Cache-Control security ensures that sensitive data, such as personal information, authenticated session states, and private API responses, are not inadvertently stored by browsers, corporate proxies, or Content Delivery Networks (CDNs). To secure sensitive pages and prevent unauthorized access or cache poisoning attacks, always apply the Cache-Control: no-store directive. Do not rely solely on no-cache, as it still permits local storage but requires revalidation. For static public assets, public, max-age=31536000, immutable is acceptable.


1. Introduction to Cache-Control and Web Security

Performance and security are often viewed as opposing forces in web development. To make websites blazingly fast, developers aggressively cache content at the browser level and across global CDNs. However, when caching is applied blindly to sensitive, authenticated endpoints, the results can be catastrophic.

At SecHead, we regularly encounter data breaches and severe vulnerabilities stemming from misconfigured HTTP caching headers. In this comprehensive guide, we'll dive deep into Cache-Control Security, explore the terrifying mechanics of Cache Poisoning and Web Cache Deception, dissect the critical differences between directives like no-store and no-cache, and provide robust configuration guides for Apache, Nginx, and Node.js.

By the end of this guide, System Administrators, Security Engineers, and Web Developers will possess a profound understanding of HTTP Caching and how to leverage it safely.


2. Understanding HTTP Caching Architecture

Before discussing security flaws, we must understand how HTTP caching operates across the modern web stack. A typical HTTP request traverses multiple layers:

  1. Browser Cache (Private Cache): The user's local machine stores assets (HTML, CSS, JS) to avoid redownloading them on subsequent visits or when navigating back.
  2. Forward Proxies / Corporate Firewalls: Organizations often cache outbound requests to save bandwidth and speed up access for their employees.
  3. Reverse Proxies / CDNs (Shared Caches): Services like Cloudflare, Akamai, or local Varnish/Nginx caches sit in front of the web server, absorbing traffic spikes by serving cached responses to thousands of users simultaneously.

When a server responds, it uses the Cache-Control header to dictate the rules of engagement for every cache layer along the path.

HTTP/2 200 OK
Date: Mon, 22 Jun 2026 08:00:00 GMT
Content-Type: application/json
Cache-Control: private, max-age=3600

{"user_id": 994, "balance": "$4,500.00", "status": "authenticated"}

In the example above, the server explicitly instructs caches that the response is private (only for the browser cache, not CDNs) and is fresh for 3600 seconds.


3. The Security Implications of Caching Failures

Failing to properly manage Cache-Control headers introduces severe risks that can compromise user data and platform integrity.

3.1. The "Back Button" Vulnerability

Imagine a user logs into a banking application from a public library computer to check their balance. They finish, log out, and walk away. If the banking portal didn't strictly instruct the browser not to store the dashboard, the next person to sit at the computer can simply press the browser's "Back" button to view the victim's cached dashboard. Even though the session token is invalidated on the server, the browser blindly renders the locally stored HTML snapshot.

3.2. Shared Cache Data Leaks (Web Cache Deception)

A far more devastating scenario involves CDNs and shared caches. Suppose your application uses a dynamically generated page at /profile/settings. If the server doesn't specify a Cache-Control header, or erroneously uses public, a CDN might cache the response generated for Alice and serve it to Bob, Charlie, and Dave.

🚨 **CRITICAL SECURITY RISK:** Web Cache Deception occurs when an attacker tricks a shared cache into storing a victim's sensitive, authenticated content, which the attacker then retrieves.

If a CDN is configured to blindly cache anything ending in .css or .jpg, an attacker might lure a victim to visit /profile/settings/avatar.jpg (which the application handles by returning the HTML profile page). The CDN caches this sensitive HTML because of the file extension, allowing the attacker to request /profile/settings/avatar.jpg and steal the victim's data.

3.3. Web Cache Poisoning

While Web Cache Deception leaks data to an attacker, Cache Poisoning allows an attacker to inject a malicious payload into the cache, which is then served to all subsequent users.

This typically involves exploiting unkeyed inputs-HTTP headers like X-Forwarded-Host or X-Original-URL that the web application uses to construct responses but the CDN does not use as part of the cache key.

GET /login HTTP/1.1
Host: sechead.com
X-Forwarded-Host: evil-attacker.com

If the server blindly trusts X-Forwarded-Host and generates a relative link like <script src="https://evil-attacker.com/app.js"></script>, the CDN caches this poisoned response. Every legitimate user who subsequently visits /login will receive the attacker's malicious JavaScript, leading to massive Cross-Site Scripting (XSS) compromise.

URL: https://sechead.com/login
SSL: Valid
Status: Warning - Page Contains Insecure Scripts

[ Alert Box: XSS Executed! ]

4. Decoding Cache-Control Directives

To defend against these threats, you must master the vocabulary of the Cache-Control header. Let's break down the most critical directives from a security perspective.

The Misunderstood: no-cache

Contrary to its intuitive name, no-cache does not mean "do not store this file." It means "you may store this file, but you must ask the server if it has changed (revalidate) before serving it to the user." If you use no-cache on a sensitive page, the browser writes the sensitive data to the local hard drive. If the hard drive is compromised, or forensics are run, the data is exposed.

The Champion: no-store

no-store is the absolute strictest directive. It explicitly forbids the browser, and all intermediate caches, from storing the response anywhere-not in memory, not on disk. This is the mandatory directive for any authenticated or sensitive data.

The Isolationist: private

private allows the end-user's local browser cache to store the file, but forbids any shared caches (CDNs, proxies) from storing it. It is useful for personalized data that isn't highly sensitive but shouldn't be served to other users.

The Exhibitionist: public

public allows anyone-browsers, proxies, CDNs-to cache the response. Never use this for user-specific or authenticated data.

Revalidation Forcers: must-revalidate and proxy-revalidate

must-revalidate tells caches that once the max-age expires, under no circumstances can they serve stale content; they must check with the origin server.


5. Security Best Practices for HTTP Caching

A robust Cache-Control strategy relies on a few fundamental rules:

  1. Default to Deny for Dynamic Content: Any route that handles user sessions, API endpoints, or dynamic HTML should output Cache-Control: no-store.
  2. Explicitly Cache Static Assets: For images, CSS, and JS files that do not contain sensitive data, use Cache-Control: public, max-age=31536000, immutable. Use file hashing in your build process (e.g., app.v2a9f.js) so you never have to worry about cache invalidation.
  3. Use the Vary Header Carefully: Ensure your server emits Vary: Cookie or Vary: Authorization if you are relying on caches for semi-dynamic content, though this is difficult to scale securely.
  4. Disable Heuristic Caching: If a server omits the Cache-Control header entirely, browsers and proxies may employ "heuristic caching" and guess how long to store the file based on the Last-Modified header. Always explicitly set Cache-Control.
  5. Separate Static and Dynamic Domains: Serve static assets from a distinct domain (e.g., static-sechead.com) where aggressive caching is safe, and keep your API/dynamic application on a domain where strict no-store rules apply globally.

6. Implementation Guides

Here is how you implement strict security caching in various popular server environments.

6.1. Nginx Configuration

In Nginx, it is best practice to apply strict headers at the server level, and selectively loosen them for specific static asset locations.

server {
    listen 443 ssl http2;
    server_name myapp.sechead.com;

    # Apply strict no-store globally for dynamic content
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
    add_header Pragma "no-cache" always;
    add_header Expires "0" always;

    location / {
        proxy_pass http://backend_app;
    }

    # Loosen restrictions for static assets
    location ~* \.(?:css|js|jpg|svg|woff2)$ {
        # Remove the strict header first
        more_clear_headers Cache-Control Pragma Expires;
        
        # Add the aggressive public cache header
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }
}

6.2. Apache Configuration

For Apache, we utilize mod_headers to enforce our directives.

<VirtualHost *:443>
    ServerName myapp.sechead.com

    # Default to secure, non-caching behavior
    Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
    Header set Pragma "no-cache"
    Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"

    # Override for static directories
    <Directory "/var/www/html/assets">
        Header unset Pragma
        Header unset Expires
        Header set Cache-Control "public, max-age=31536000, immutable"
    </Directory>
</VirtualHost>

6.3. Node.js (Express) Configuration

When building an API or a server-side rendered application in Express, we highly recommend using the helmet middleware. Helmet's noSniff and general headers are great, but for caching, use explicit middleware.

const express = require('express');
const app = express();

// Secure middleware for all dynamic routes
const preventCaching = (req, res, next) => {
    res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
    res.set('Pragma', 'no-cache');
    res.set('Expires', '0');
    next();
};

// Apply to API routes
app.use('/api', preventCaching);

app.get('/api/user/profile', (req, res) => {
    res.json({ id: 1, name: "Admin", email: "admin@sechead.com" });
});

// Use express.static for safe public assets with aggressive caching
app.use('/static', express.static('public', {
    maxAge: '1y',
    immutable: true
}));

app.listen(3000);
$ curl -I https://myapp.sechead.com/api/user/profile
HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
Pragma: no-cache
Expires: 0

7. Advanced Defense: Preventing Cache Poisoning

Implementing no-store protects against data leakage, but what about Cache Poisoning on routes that are cached?

To prevent Web Cache Poisoning, Security Engineers must ensure that:

  1. Unkeyed Inputs are Ignored: Do not reflect unkeyed headers (like X-Forwarded-Host, X-Forwarded-Scheme, or X-Original-URL) in your application's response. If your application relies on these headers to build URLs, ensure your CDN is configured to include them in the Cache Key.
  2. Fat GET Requests are Rejected: Some applications process GET requests with a body (which is out of spec but technically possible). If a proxy ignores the body when caching, an attacker can cache a response altered by a malicious body. Reject GET requests that contain a body.
  3. Parameter Cloaking is Mitigated: Ensure your WAF and application handle parameter parsing identically. For example, if the application reads ?param=1;param=2 but the cache only keys on ?param=1, attackers can exploit the discrepancy.

8. People Also Ask (PAA)

Does Cache-Control: no-cache mean no caching? No, no-cache simply forces the browser or proxy to revalidate the content with the origin server before serving it. It still allows the response to be stored on the physical disk. To completely prevent storage, you must use no-store.

What is the difference between Cache-Control and Pragma headers? Pragma: no-cache is an archaic HTTP/1.0 header. While Cache-Control (introduced in HTTP/1.1) is the modern standard, Pragma: no-cache is often still included for backwards compatibility with extremely old proxy servers.

How do CDNs handle Cache-Control headers? CDNs usually respect the Cache-Control header provided by the origin server. If the origin sends public, max-age=3600, the CDN caches it for an hour. If the origin sends no-store, the CDN acts as a simple pass-through and refuses to cache the response.

Can I use max-age=0 instead of no-store? max-age=0 forces immediate expiration, meaning the cache must revalidate instantly. However, it still allows the browser to store the file locally. For sensitive data, max-age=0 is insufficient; you need no-store to prevent disk writes.


9. Comprehensive FAQ Section

Q1: How does the s-maxage directive differ from max-age? A1: While max-age applies to all caches (browsers and CDNs alike), s-maxage (shared max-age) specifically targets shared caches like CDNs. It overrides max-age or the Expires header. This allows you to tell a browser to cache something for 10 minutes (max-age=600), but tell the CDN to cache it for a day (s-maxage=86400).

Q2: If I use no-store, do I still need to use no-cache and must-revalidate? A2: According to the HTTP specification, no-store is sufficient and overrides all other directives. However, in practice, due to buggy implementations in legacy enterprise proxies or obscure browser builds, it is common to chain them together as a robust fallback: Cache-Control: no-store, no-cache, must-revalidate.

Q3: What happens if there is no Cache-Control header present? A3: If missing, browsers and proxies may fall back to "Heuristic Caching." They will estimate a cache duration, often calculated as a percentage of the time since the Last-Modified header. This unpredictable behavior is why omitting the header is a security risk.

Q4: How do I test if my web application is vulnerable to Web Cache Deception? A4: You can append a static file extension to a dynamic route (e.g., changing /account/dashboard to /account/dashboard/fake.css). If the application loads your dashboard but the response headers indicate a cache HIT from the CDN, you are highly vulnerable.

Q5: What is the immutable directive? A5: immutable tells the browser that the response body will never change over time. This prevents the browser from sending conditional revalidation requests (like If-None-Match) when the user refreshes the page, saving significant bandwidth for hashed static assets.

Q6: Can Cache-Control headers secure my API? A6: Yes. APIs often return sensitive JSON data. An API endpoint returning user profiles, API keys, or financial data should always include Cache-Control: no-store.

Q7: Is Cache-Control: private safe enough for sensitive data? A7: No. private prevents CDNs from caching the data, but it still allows the user's browser to store it on the hard drive. If the user is on a shared computer, the data can be recovered. Always use no-store for sensitive data.

Q8: Why does my application still cache even when I send no-store? A8: Sometimes, an intermediate proxy or CDN is configured to actively strip or ignore origin Cache-Control headers and apply its own forced caching rules. You must audit your CDN (like Cloudflare Page Rules) to ensure it respects origin headers.

Q9: What role does the ETag header play in caching? A9: An ETag (Entity Tag) is a unique identifier (often a hash) representing a specific version of a resource. It is used for cache validation. If a browser has a cached file with an ETag, it can ask the server "Is this ETag still valid?" using the If-None-Match request header.

Q10: Should I use the Expires header? A10: The Expires header is an HTTP/1.0 standard specifying an absolute date/time when the cache expires. Cache-Control: max-age (HTTP/1.1) is superior because it uses relative time and prevents clock synchronization issues between the client and server. max-age overrides Expires.

Q11: How do Service Workers interact with Cache-Control? A11: Service Workers intercept network requests and can serve responses from the Cache Storage API. Service Workers operate independently of HTTP Cache-Control headers; you must explicitly code your Service Worker logic to avoid caching sensitive endpoints.

Q12: Can Web Cache Poisoning lead to Remote Code Execution (RCE)? A12: Typically, no. Cache Poisoning usually results in Cross-Site Scripting (XSS), Data Exfiltration, or Denial of Service (DoS). However, if an internal administrative dashboard consumes the poisoned cache and executes privileged operations, it could theoretically pivot into a deeper compromise.

Q13: How does the Vary header prevent caching issues? A13: The Vary header tells caches which request headers should be included in the cache key. For example, Vary: Accept-Encoding ensures that the GZIP version of a file is cached separately from the Brotli version.

Q14: Can I use Vary: User-Agent safely? A14: It is generally discouraged. Because there are thousands of distinct User-Agent strings, using Vary: User-Agent will create a massive number of cache entries (cache fragmentation), destroying your cache hit ratio and degrading CDN performance.

Q15: What is the proxy-revalidate directive? A15: It is similar to must-revalidate, but it specifically only applies to shared caches (proxies and CDNs), leaving the user's private browser cache unaffected.


10. Conclusion: Caching with Confidence

Implementing HTTP caching securely is not just about sprinkling no-store on random endpoints. It requires a fundamental shift in how developers design their architecture. Treat caching as a boundary: distinct, mathematically hashed static files live in a world of aggressive public, immutable caching, while dynamic, authenticated routing lives in a strict no-store lockdown.

By understanding the severe risks of Web Cache Deception and Cache Poisoning, System Administrators and Security Engineers can craft firewall rules, CDN configurations, and application middlewares that protect users without sacrificing web performance. Always verify your configurations using curl or browser devtools, and remember: when it comes to sensitive data, storing nothing is the only true security.



Continue your journey into web security with these related, deep-dive articles from the SecHead team:

SEO Metadata

  • Meta Title: Cache-Control Security: Preventing Data Leaks & Cache Poisoning
  • Meta Description: Master Cache-Control header security. Learn how to stop browsers and CDNs from storing sensitive data, prevent Web Cache Poisoning, and configure Nginx/Apache.
  • URL Slug: cache-control-security
  • Target Keywords: Cache-Control Security, Cache Poisoning, no-store, no-cache, HTTP Caching, Web Cache Deception

Related articles

Free tool

Check your own security headers

Instant grade, plain-language explanations, and a full remediation plan - no signup needed.

Scan your site now →