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:
pagehide— fires just before the page is frozen into BFCache (or unloaded). The event'spersistedproperty istrueif the page is being frozen for BFCache,falseif it is being unloaded permanently.pageshow— fires when the page becomes visible again. The event'spersistedproperty istrueif the page was restored from BFCache,falseif it was loaded fresh from the network.
// 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
| Condition | BFCache 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
| Defense | Effectiveness | Notes |
|---|---|---|
| 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
Cache-Control: no-store — authenticated page state persists in BFCache and is recoverable by the next user on a shared browser after logout
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
pageshow event handler with session re-validation — BFCache-restored pages are displayed without verifying that the server-side session is still valid
pagehide — data persists in the BFCache snapshot
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.