Security Guide

MCP server Content Index API security — offline content enumeration, service worker surveillance, and user interest inference

The Content Index API allows service workers to register content for offline availability and expose it in the browser's offline content UI. registration.index.getAll() returns every item registered at the origin — article titles, URLs, descriptions, categories, and icons. MCP server tool output that can reach the service worker registration can enumerate this index to map the user's saved reading material: what topics they're researching, what articles they've bookmarked for offline access, and what categories of content they consume. This is passive browsing history inference from structured metadata, requiring no interaction beyond injecting the enumeration script.

What the Content Index API exposes

The Content Index API (Chrome 84+, available on Android Chrome) is designed for news readers, recipe apps, and any PWA that wants users to access content while offline. An application registers articles and pages via registration.index.add() with a descriptor that includes the content's title, description, URL, category ('article', 'video', 'audio', 'homepage'), icons, and publication date. The browser surfaces this in its offline content browser, and the service worker provides the cached content when offline.

The attack surface: registration.index.getAll() is readable by any same-origin JavaScript that can obtain the service worker registration. The returned array contains complete descriptors for every piece of content the user has saved for offline reading at the origin — a direct window into the user's content consumption history.

// Content Index enumeration — reads all user-saved offline content
// Requires access to service worker registration (same-origin)
async function enumerateContentIndex() {
  const registration = await navigator.serviceWorker.ready;

  // getAll() returns all content registered for offline access at this origin
  const contentItems = await registration.index.getAll();

  // Each item contains:
  // { id, title, description, url, category, icons: [{src, sizes, type}] }
  const profile = contentItems.map(item => ({
    title: item.title,         // "How to Migrate from OpenAI to Claude API"
    description: item.description,  // Article summary — topic and detail level
    url: item.url,             // Full URL — reveals source publication and topic
    category: item.category,  // 'article' | 'video' | 'audio' | 'homepage'
    // launch URL if different from content URL (may reveal additional path params)
    launchUrl: item.launchUrl
  }));

  // This array reveals:
  // - Research topics the user is investigating
  // - Publications and sources the user reads
  // - Content categories (news, technical, financial, political)
  // - Recency of interest (items added recently vs long ago)
  navigator.sendBeacon('/track', JSON.stringify({
    contentIndex: profile,
    count: profile.length
  }));
}

Content Index items reveal structured interest profiles with semantic labels. Unlike browser history (URLs without descriptions) or cache entries (opaque blobs), Content Index items include human-readable titles and descriptions — the same metadata the user sees when browsing offline content. An attacker who reads this array gets a curated, semantically labeled view of what the user intended to read and research, without any interaction beyond the injection.

Modifying the Content Index: polluting offline content

Beyond reading the index, injected code can add items to it via registration.index.add(). Adding attacker-controlled items to the Content Index has two effects: (1) the items appear in the browser's offline content UI, potentially confusing the user about what they saved, and (2) the service worker may cache and serve attacker-supplied content under the origin, creating a persistent content injection that survives until explicitly removed.

This is distinct from Cache Storage manipulation because Content Index items appear in a dedicated browser UI (the offline content browser on Android Chrome) that users may treat as authoritative "saved reading" from their application. Injecting plausible-looking articles into this list could direct users to attacker-controlled content the next time they browse their offline reading list.

AttackContent Index surfaceWhat it enables
Offline content enumeration registration.index.getAll() All saved articles/videos with titles, descriptions, URLs — user interest profile
Browsing pattern inference Content categories and URLs Topics, sources, and content types reveal research context and media habits
Content Index pollution registration.index.add() Injects attacker content into user's offline reading list — content confusion and redirection
Content Index deletion DoS registration.index.delete(id) Removes user's saved offline content — data loss that disrupts offline workflows

Permissions-Policy gap and defenses

There is no Permissions-Policy directive for the Content Index API. The API is accessible to any same-origin JavaScript with a service worker registration. The primary defenses are: (1) cross-origin sandboxed iframe rendering for MCP tool output — injected code runs at a different origin and cannot reach the application's service worker registration or content index; (2) CSP script-src with nonces — prevents the injection from executing the enumeration script; (3) MCP applications that do not use a service worker at all have no content index exposure, since navigator.serviceWorker.ready returns a pending promise that never resolves if no SW is registered at the scope.

MCP deployments without a service worker are not exposed to Content Index attacks. The Content Index API only has data if the application uses a service worker and that SW calls index.add(). MCP clients that rely solely on HTTP caching and do not register a service worker have no Content Index to enumerate. This makes Content Index a tier-2 risk that applies specifically to PWA-based MCP clients.

SkillAudit findings for Content Index API exposure

High MCP client uses a service worker with Content Index; tool output rendered same-origin can enumerate all saved offline content titles, descriptions, and URLs. Content Index items represent a curated, semantically labeled user interest profile — more actionable for profiling than raw cache entries. Grade impact: −16.
High No cross-origin iframe isolation; injected JS can reach navigator.serviceWorker.ready and access the content index without any permission prompt. Service worker registration is available to all same-origin JavaScript — no additional permission gate exists for the Content Index API specifically. Grade impact: −14.
Medium Content Index write access allows injected code to add attacker-controlled items to user's offline reading list. Polluting the offline content list with plausible-looking articles can redirect users to attacker-controlled content when they browse their saved offline material. Grade impact: −12.
Medium registration.index.delete() allows injected code to remove user's saved offline content — destructive without any confirmation UI. Data loss of saved offline content disrupts users who rely on offline reading for commutes, travel, or poor-connectivity environments. Grade impact: −10.
Low No Permissions-Policy gate available for Content Index API; cannot be blocked via HTTP headers. Unlike camera, geolocation, or background-fetch, Content Index has no policy directive. Only architectural isolation (cross-origin iframe) and script injection prevention (CSP) provide control. Grade impact: −5.

Audit your MCP server for Content Index and service worker risks

SkillAudit checks for tool output isolation, service worker attack surfaces, and Content Index exposure — paste a GitHub URL and get a graded security report in 60 seconds.

Run a free audit →