MCP Server Security · Electron Security

MCP server Electron security — nodeIntegration, contextIsolation, remote module, webSecurity, and preload script hardening for Electron-based MCP clients

Electron-based MCP clients ship a full Chromium browser and Node.js runtime in the same process. Unlike browser-based MCP UIs that are constrained to the web security model, an Electron app with permissive renderer configuration can expose Node.js APIs — fs, child_process, net — directly to the JavaScript running in the renderer. If an MCP tool returns attacker-controlled content that reaches the renderer via prompt injection, and the renderer has Node.js access, the attacker has full OS-level code execution.

The Electron threat model for MCP clients

The threat is not Electron itself — it's misconfigured Electron that treats renderer web content with the same trust as main-process code. Electron's security model separates the main process (full Node.js, OS access) from renderer processes (web content, should be sandboxed). The renderer-to-main IPC bridge is the intended communication path. The danger is when that bridge is bypassed or when Node.js APIs are exposed directly to renderer JavaScript.

In MCP clients, the renderer displays agent conversation output and renders tool call results. If the renderer displays attacker-controlled content (via prompt injection in a tool response), any Node.js API exposed to the renderer is reachable by the attacker.

Critical Electron security settings for MCP BrowserWindow

import { BrowserWindow } from 'electron';
import path from 'path';

function createMCPWindow() {
  const win = new BrowserWindow({
    webPreferences: {
      // CRITICAL: nodeIntegration must be false
      // If true, all renderer JavaScript can access require('child_process'),
      // require('fs'), etc. — attacker-controlled tool output = OS code execution
      nodeIntegration: false,

      // CRITICAL: contextIsolation must be true (default since Electron 12)
      // Creates a separate V8 context for preload scripts so they share no
      // JavaScript heap with renderer web content. Without this, renderer code
      // can access preload functions by walking the prototype chain.
      contextIsolation: true,

      // Enable renderer sandbox (Chromium process sandbox)
      // Prevents renderer process from making direct syscalls to the OS
      sandbox: true,

      // CRITICAL: webSecurity must be true (default)
      // Setting to false disables same-origin policy — allows the renderer to
      // make cross-origin requests, read arbitrary file:// URLs, and bypass CORS
      webSecurity: true,

      // Specify a preload script for IPC bridge
      // The preload script is the ONLY code that runs with contextIsolation privileges
      preload: path.join(__dirname, 'preload.js'),

      // Disable remote module (removed in Electron 14, but guarding older versions)
      // enableRemoteModule: false,  // deprecated and removed

      // Allow only specific origins to load
      // additionalArguments: ['--disable-web-security'] is NEVER acceptable
    },
  });

  // Restrict what URLs the window can navigate to
  win.webContents.on('will-navigate', (event, navigationUrl) => {
    const parsed = new URL(navigationUrl);
    const ALLOWED_ORIGINS = new Set([
      'https://skillaudit.dev',
      'https://api.skillaudit.dev',
    ]);
    if (!ALLOWED_ORIGINS.has(parsed.origin)) {
      event.preventDefault();
    }
  });

  // Block new window creation from renderer (prevents pop-under attacks)
  win.webContents.setWindowOpenHandler(({ url }) => {
    // Deny all new window creation from renderer
    // Open in external browser instead
    require('electron').shell.openExternal(url);
    return { action: 'deny' };
  });

  return win;
}

The remote module is permanently retired. If your Electron MCP client uses require('electron').remote, the entire main process object graph is accessible from renderer JavaScript. This was deprecated in Electron 9 and removed in Electron 14. If you're on an older version, removing the remote module is a prerequisite for any security hardening — it is impossible to secure a renderer that has access to the remote module.

Preload script hardening with contextBridge

With contextIsolation: true, the preload script runs in a privileged context but cannot share JavaScript objects directly with the renderer. The contextBridge.exposeInMainWorld API is the approved channel — it creates a one-way, serialization-safe bridge that exposes only named functions to renderer JavaScript. The renderer cannot reach any preload variables or Node.js requires not explicitly exposed through contextBridge.

// preload.js — the ONLY code with contextIsolation privileges
const { contextBridge, ipcRenderer } = require('electron');

// Expose a minimal surface area — only the operations the UI actually needs
contextBridge.exposeInMainWorld('mcpBridge', {
  // Invoke MCP tool calls via main process IPC — renderer never gets raw Node.js
  invokeTool: (toolName: string, params: unknown) => {
    // Validate parameters before sending to main process
    if (typeof toolName !== 'string' || toolName.length > 128) {
      throw new Error('invalid tool name');
    }
    return ipcRenderer.invoke('mcp:tool', { toolName, params });
  },

  // Expose only what's needed, never fs, child_process, or net directly
  openExternalUrl: (url: string) => {
    // Validate URL scheme before asking main process to open it
    const parsed = new URL(url);
    if (!['https:', 'http:'].includes(parsed.protocol)) {
      throw new Error('Blocked: only https:// and http:// URLs may be opened externally');
    }
    return ipcRenderer.invoke('shell:openExternal', url);
  },

  // No: do NOT expose require, __dirname, process, or Node built-ins
  // These lines would be catastrophic:
  // fs: require('fs'),                    // NEVER
  // exec: require('child_process').exec,  // NEVER
  // process: process,                     // NEVER
});

CSP in Electron renderer

Electron respects Content-Security-Policy meta tags and HTTP headers in the renderer. In production Electron apps, set CSP via the session.webRequest API to ensure it applies to all loaded resources, not just the initial HTML:

import { session } from 'electron';

app.whenReady().then(() => {
  // Inject CSP header into all responses loaded in the main window session
  session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
    callback({
      responseHeaders: {
        ...details.responseHeaders,
        'Content-Security-Policy': [
          "default-src 'none'; " +
          "script-src 'self'; " +        // Only scripts bundled with the app
          "style-src 'self'; " +
          "img-src 'self' data: https:; " +
          "connect-src 'self' https://api.skillaudit.dev; " +
          "frame-src 'none'; " +
          "object-src 'none'; " +
          "base-uri 'none'"
        ],
      },
    });
  });
});

SkillAudit findings for Electron MCP client security

CRITICAL −24nodeIntegration: true in BrowserWindow webPreferences — renderer JavaScript (including attacker-controlled tool output via prompt injection) has full access to Node.js APIs: require('child_process').exec, require('fs').readFile, OS process spawning
CRITICAL −22contextIsolation: false — preload script's privileged context shares a V8 heap with renderer JavaScript; attacker can access preload-bound Node.js modules by traversing the prototype chain
HIGH −18remote module enabled (Electron <14) — entire Electron main process API surface including app, BrowserWindow, session, and IPC handlers accessible from renderer JavaScript
HIGH −16webSecurity: false — disables same-origin policy; renderer can make cross-origin requests, read file:// paths, and bypass CORS on behalf of an injected attacker script
HIGH −14No will-navigate handler — renderer can navigate to file://, custom scheme deep links, or external attackers' URLs without restriction
MEDIUM −10Preload script exposes Node.js built-ins (fs, path, child_process) via contextBridge.exposeInMainWorld — defeats contextIsolation; the surface area should be minimal named functions, not module references
MEDIUM −8No CSP in Electron renderer — inline scripts and arbitrary external resource loads allowed; prompt injection in tool output can execute scripts if renderer HTML is constructed dynamically

See also: Deep link injection · VM sandbox security · Process isolation

Run a free SkillAudit on your MCP server →