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.
| Attack | OPFS surface | What 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
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 →