Security Guide

MCP server Origin Private File System security — no-permission persistent storage, cross-tab access, and storage-clear evasion

The Origin Private File System (OPFS) provides browser JavaScript with a sandboxed filesystem at navigator.storage.getDirectory() — no user permission prompt, no user-visible path, accessible to all same-origin JavaScript. MCP server tool output that can inject JavaScript into the main document gains write access to a persistent, quota-allocated file store that survives cookies clear, sessionStorage clear, and in many browsers, partial Clear Browsing Data actions. OPFS enables two distinct attack classes: persistent data staging across MCP sessions, and synchronous high-throughput file writes via OPFS AccessHandles in Web Workers.

What OPFS provides and why it requires no permission

The Origin Private File System is part of the File System Access API specification (distinct from the user-facing file picker API). While showOpenFilePicker() and showSaveFilePicker() require explicit user consent because they access the host OS filesystem, OPFS is sandboxed entirely within the browser's origin partition and is treated as a managed storage resource — similar to IndexedDB or Cache Storage — that does not require a user permission gate.

Any same-origin JavaScript can call await navigator.storage.getDirectory() to obtain the root FileSystemDirectoryHandle and from there read, write, create, and delete files without any prompt. Browser quota applies (typically up to 60% of available disk space), but no per-access permission is required. This is architecturally intentional: OPFS is meant for high-performance application storage (SQLite WASM databases, large asset caches, offline editor state), not for user-visible files.

// OPFS access — no permission prompt required
const root = await navigator.storage.getDirectory();

// Create or open a file in OPFS
const fileHandle = await root.getFileHandle('staged-payload.json', { create: true });
const writable = await fileHandle.createWritable();

// Write stolen data — this call is asynchronous (main thread)
const stolen = {
  cookies: document.cookie,
  storage: { ...localStorage },
  url: location.href,
  timestamp: Date.now()
};
await writable.write(JSON.stringify(stolen));
await writable.close();
// File is now persisted in OPFS — survives page reload, localStorage.clear(), cookies.clear()

OPFS is not cleared by "Clear cookies and site data" unless the user explicitly selects "Cached images and files" AND the browser includes OPFS in that category. In Chrome, OPFS is cleared by "All time" + "Cached images and files" but NOT by the commonly-used "Cookies and other site data" checkbox alone. Partial storage clears leave OPFS intact.

OPFS AccessHandles in Web Workers: synchronous, high-throughput writes

The OPFS specification includes a Web Worker-only API: FileSystemFileHandle.createSyncAccessHandle(). In a dedicated or shared worker context, this returns a synchronous access handle that allows accessHandle.write(buffer, { at: offset }) calls without any Promise overhead. This is the fastest possible file write path in the browser — used by SQLite WASM for its WAL implementation.

For MCP tool output attackers, this matters because: (1) if the injection can spawn a worker (or reach an existing one), synchronous OPFS writes enable high-throughput staging of large data volumes before the user's tab becomes unresponsive; (2) the synchronous nature means the entire staging operation can complete in a single worker turn, reducing the time window in which CPU profiling tools might observe the work.

// In an injected Web Worker — synchronous OPFS write
// worker.js (injected or reached via postMessage)
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('sync-payload.bin', { create: true });

// createSyncAccessHandle() is only available in dedicated/shared workers
const accessHandle = await fileHandle.createSyncAccessHandle();

// Synchronous write — no Promise overhead
const encoder = new TextEncoder();
const buffer = encoder.encode(JSON.stringify(largePayload));
accessHandle.write(buffer, { at: 0 });
accessHandle.flush();
accessHandle.close();
// Write completes synchronously — no async gaps, minimal CPU profile exposure

Cross-tab data access via shared OPFS scope

All tabs and workers at the same origin share the same OPFS namespace. A file written to OPFS in one tab is immediately readable from another tab at the same origin. For MCP deployments where a user opens multiple tool sessions or switches between chat windows at the same origin, an OPFS file written in one session is accessible in all concurrent sessions. An injection that writes to OPFS in a low-privilege session (e.g., a tab with restricted tool access) can have its data read by a higher-privilege session at the same origin.

AttackOPFS surfaceWhat it enables
Cross-session data staging getDirectory() → file write Stolen data persists across page reloads; exfiltrated in a later session when monitoring is lower
Storage-clear evasion OPFS not cleared by partial site data clears User clears cookies and localStorage; OPFS payload remains intact
High-throughput worker staging createSyncAccessHandle() in worker Synchronous GB-scale write before page close; no async gaps in CPU profile
Cross-tab exfiltration read Shared OPFS scope across origin High-privilege tab reads OPFS file written by low-privilege injection in another tab
Quota consumption DoS OPFS quota up to ~60% of disk Filling OPFS to quota prevents legitimate application storage writes (IndexedDB, Cache API)

Permissions-Policy gap and defenses

As of mid-2026, there is no Permissions-Policy feature name for OPFS. Unlike the user-facing File System Access API (which has file-system in the policy), OPFS is not gated by a Permissions-Policy directive. The primary defenses are architectural:

Cross-origin sandboxed iframe — if MCP tool output is rendered in a sandboxed iframe at a distinct registrable domain (not a subdomain of the main application), the iframe has its own OPFS partition and cannot read or write the application's OPFS. The sandbox attribute on iframes restricts storage access; allow-same-origin must not be combined with allow-scripts on same-origin iframes or the sandbox is bypassed.

CSP script-src nonce — prevents the script injection that would reach navigator.storage.getDirectory() in the first place. Without script execution, OPFS is not accessible.

SkillAudit findings for OPFS exposure

High Tool output rendered same-origin; injected JS can write persistent data to OPFS without permission. OPFS is available to any same-origin JavaScript. Stolen session data written to OPFS survives cookies clears and partial site data removals. Grade impact: −18.
High No cross-origin iframe isolation; OPFS written in one tool session readable by all concurrent same-origin tabs. Cross-session and cross-tab OPFS access allows low-privilege injection to stage data readable by higher-privilege sessions. Grade impact: −16.
Medium Web Worker accessible from injected code; OPFS AccessHandle enables synchronous high-throughput write before tab close. Synchronous OPFS writes via createSyncAccessHandle() complete faster than async paths and produce smaller CPU profiling signatures. Grade impact: −12.
Medium No OPFS audit in incident response runbook; partial storage clears leave staged payload intact. If the response to a suspected injection clears cookies and localStorage but not OPFS, staged exfiltration data persists and can be retrieved in subsequent sessions. Grade impact: −10.
Low OPFS quota consumption possible via repeated large writes; legitimate application IndexedDB/Cache API writes may fail. Filling the origin's storage quota denies legitimate storage operations, causing application errors that may prompt users to clear all site data — removing legitimate application state. Grade impact: −6.

Audit your MCP server for OPFS storage risks

SkillAudit checks for tool output isolation, script injection vectors, and missing storage security controls — paste a GitHub URL and get a graded security report in 60 seconds.

Run a free audit →