Security Guide

MCP server Back/Forward Cache (BFCache) security — auth state persistence, session leakage, and Cache-Control: no-store defense

The Back/Forward Cache (BFCache) is a browser optimization in Chrome, Firefox, and Safari that freezes the entire page snapshot — JavaScript heap, DOM state, event listeners, timers, credentials, and authenticated UI — into memory when the user navigates away. On back or forward navigation, the frozen snapshot is restored in milliseconds without re-running any JavaScript or contacting the server. MCP tool output that handled sensitive data — auth tokens, API keys, private messages, file contents, user PII — persists in the BFCache copy. An attacker who can trigger back navigation after a session logout retrieves the authenticated page state without the server ever knowing. Cache-Control: no-store is the most reliable mechanism to exclude MCP renderer pages from BFCache.

How BFCache works across Chrome, Firefox, and Safari

All three major browser engines implement BFCache, though with different eligibility rules. When the user navigates away from a page, the browser evaluates whether the page qualifies for BFCache. If it does, the browser suspends the page's JavaScript execution and serializes the full execution context — including the heap — into an in-memory cache slot. When the user presses back or forward, the browser restores the page from this cache slot rather than re-fetching the URL.

Two lifecycle events bracket the BFCache transition:

// Detecting BFCache freeze and restoration

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    // Page is being frozen into BFCache — NOT unloaded
    // Any sensitive state in memory will survive here
    console.log('Page frozen into BFCache — credentials and tool output persist in memory');
  } else {
    // Normal unload — page is being destroyed
    console.log('Page is being unloaded — memory will be freed');
  }
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Page was restored from BFCache — no network request was made
    // JavaScript heap is exactly as it was when the page was frozen
    console.log('Page restored from BFCache — session state may be stale');

    // DEFENSE: Re-validate session on BFCache restoration
    fetch('/api/session/validate', { credentials: 'include' })
      .then(res => {
        if (!res.ok) {
          // Session is no longer valid — redirect to login
          window.location.replace('/login');
        }
      });
  }
});

Testing whether a navigation was served from BFCache

The Navigation Timing Level 2 API exposes the navigation type, which distinguishes BFCache restorations from normal navigations. Security engineers can use this to verify whether logout flows correctly exclude pages from BFCache or whether a given page is BFCache-eligible.

// Test whether the current page load was served from BFCache
// Useful for verifying that logout and sensitive pages are excluded

function wasRestoredFromBFCache() {
  const navEntries = performance.getEntriesByType('navigation');
  if (navEntries.length === 0) return false;
  // type === 'back_forward' means the page was re-navigated via back/forward
  // Combined with event.persisted === true in pageshow, confirms BFCache restoration
  return navEntries[0].type === 'back_forward';
}

// Run on pageshow to detect BFCache-restored back/forward navigations
window.addEventListener('pageshow', (event) => {
  const fromBFCache = event.persisted && wasRestoredFromBFCache();
  if (fromBFCache) {
    console.warn('BFCache restoration detected — re-validate all sensitive state');
  }
});

Attack surface 1: sensitive MCP tool output persists in the BFCache copy

MCP tools regularly output sensitive data into the browser rendering context: authentication tokens from an OAuth tool, private messages from a messaging integration, file contents from a file-access tool, or user PII from a CRM integration. When the user navigates away from the page displaying this output, BFCache freezes the page — including all that sensitive data — into memory. The data is not cleared, not invalidated, and not re-validated with the server.

Shared browser scenario. User A logs into an MCP-powered application, reads sensitive tool output (e.g. HR records, API credentials, private file contents), then navigates to another site and logs out. User B sits down at the same computer, opens the same browser, and presses the back button. The browser restores the BFCache snapshot — User A's authenticated session, their sensitive tool output, and all DOM state — without any server interaction. User B sees User A's data.

Attack surface 2: logout does not invalidate the BFCache copy

A common logout implementation sends a POST /logout request, invalidates the session cookie on the server, and redirects to the login page. This correctly ends the server-side session. However, it does not evict the BFCache entry for the previously authenticated page. If the attacker can trigger a back navigation immediately after logout — by using JavaScript's history.back(), a crafted link, or physical access to the browser — they land on the BFCache-restored authenticated page.

// Insecure logout implementation — BFCache entry survives
async function insecureLogout() {
  await fetch('/api/logout', { method: 'POST', credentials: 'include' });
  // Server invalidates session cookie. But the previous authenticated page
  // is still frozen in BFCache and will be served on back navigation.
  window.location.href = '/login';
}

// Secure logout implementation — prevents BFCache restoration of auth pages
async function secureLogout() {
  await fetch('/api/logout', { method: 'POST', credentials: 'include' });

  // Option 1: Navigate to login AND set a pageshow handler that re-checks auth
  // This approach works even if some pages are already in BFCache

  // Option 2: The server must respond with Cache-Control: no-store on all
  // authenticated pages — BFCache respects this header and excludes the page.
  // This is the most reliable defense (see Defenses section below).

  window.location.replace('/login'); // replace() removes current page from history
}

The unload event handler: cross-browser inconsistency

A widely-known technique for disabling BFCache is registering an unload event handler. Pages with active unload listeners are excluded from BFCache in Firefox and Safari because those browsers cannot guarantee the handler won't have observable side effects on restoration. However, Chrome has deprecated the unload event and no longer uses its presence as a BFCache eligibility criterion — meaning this technique provides false security for Chrome users.

// Adding an unload handler disables BFCache in Firefox and Safari
// but NOT in Chrome (Chrome deprecated unload event in 2024+)
// Do NOT rely on this as your primary BFCache defense

window.addEventListener('unload', () => {
  // Firefox and Safari exclude this page from BFCache
  // Chrome ignores the unload handler for BFCache eligibility
});

Chrome deprecation of unload. Chrome's unload deprecation (Intent to Deprecate launched in 2023, progressive rollout through 2024) means the unload handler technique is no longer a cross-browser BFCache defense. Pages that relied on unload to stay out of BFCache will now be BFCache-eligible in Chrome. Any MCP deployment that depended on this assumption should switch to Cache-Control: no-store.

The correct defense: Cache-Control: no-store

The Cache-Control: no-store response header is the most reliable cross-browser mechanism to exclude a page from BFCache. Chrome, Firefox, and Safari all respect this directive by refusing to store the page in BFCache. MCP server renderer endpoints that return authenticated pages or pages that may display sensitive tool output should include this header.

# Server-side response headers for MCP renderer pages that handle sensitive tool output
# Include on any route that may display authenticated content or MCP tool results

# Express.js / Node.js
app.use('/mcp-chat', (req, res, next) => {
  // no-store excludes the page from BFCache across Chrome, Firefox, and Safari
  res.setHeader('Cache-Control', 'no-store');
  // Also prevent intermediate proxy caching
  res.setHeader('Pragma', 'no-cache');
  next();
});

# Python (Flask)
@app.after_request
def add_cache_headers(response):
    if request.path.startswith('/mcp'):
        response.headers['Cache-Control'] = 'no-store'
    return response

# Nginx config
location /mcp-chat {
    add_header Cache-Control "no-store" always;
    proxy_pass http://backend;
}

BFCache eligibility criteria summary

ConditionBFCache eligible?Notes
Normal page, no special headers Yes (Chrome, Firefox, Safari) Default behavior — page is frozen on navigation away
Cache-Control: no-store in response No — excluded from BFCache Most reliable cross-browser defense; use on all sensitive MCP renderer pages
Active unload event handler Firefox/Safari: No. Chrome: Yes (unload deprecated) Unreliable; do not use as primary BFCache defense
Active IndexedDB transaction No — page is unloaded instead Indirect; not a reliable intentional defense
Open WebSocket connection No — connection would be frozen/broken Chrome excludes pages with open WebSocket connections from BFCache
Page in a cross-origin iframe Depends on outer page eligibility If outer page is excluded from BFCache, inner page is also excluded

Defenses

DefenseEffectivenessNotes
Cache-Control: no-store on all authenticated/MCP pages High — excludes pages from BFCache across all browsers Primary recommended defense; apply to any page that displays MCP tool output or authenticated content
Re-validate session on pageshow with event.persisted === true High — detects stale BFCache restorations and redirects to login Defense-in-depth; use alongside Cache-Control: no-store; protects pages that were cached before the header was added
Clear sensitive data in pagehide when event.persisted === true Medium — reduces data exposure in BFCache snapshot Clear JavaScript variables and DOM content; however, some browsers may still freeze stale snapshots
history.replaceState after logout to remove authenticated URL from history Low — prevents direct back navigation to authenticated page URL Attacker can still navigate using browser history UI or history.go(-2)
Unload event handler Low in Chrome (deprecated) — Medium in Firefox/Safari Do not rely on this; use Cache-Control: no-store instead

Findings SkillAudit reports

Critical MCP renderer pages that display auth tokens, credentials, or PII do not send Cache-Control: no-store — authenticated page state persists in BFCache and is recoverable by the next user on a shared browser after logout
High Logout flow does not call window.location.replace() — the authenticated page remains in the browser's history stack and BFCache; an attacker pressing back immediately after logout retrieves the authenticated state
High No pageshow event handler with session re-validation — BFCache-restored pages are displayed without verifying that the server-side session is still valid
Medium MCP tool output containing file contents or private messages is rendered into DOM without clearing on pagehide — data persists in the BFCache snapshot
Low MCP server documentation does not address BFCache behavior or provide guidance on Cache-Control headers for sensitive pages

Related guides: MCP server caching security, Session management security, Browser storage security.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a report covering BFCache eligibility, response header analysis, logout flow security, and your full browser permission posture in 60 seconds.