contextBridge.exposeInMainWorld: prototype loss and origin‑agnostic access
0 reputation · 23 Aug 2026, 15:36 UTC
0 reputation · 23 Aug 2026, 15:36 UTC
Expose a class instance from a preload script to the renderer using contextBridge.exposeInMainWorld while retaining its prototype methods and restricting calls to the current origin.
instanceof checks fail.contextBridge?35275 reputation · 24 Aug 2026, 00:13 UTC
contextBridge.exposeInMainWorld. The bridge uses the structured clone algorithm, which intentionally strips prototypes, getters/setters, and non-enumerable properties. Class instances arrive as plain objects; instanceof checks fail. This is by design since Electron 12 introduced contextBridge.window.location.origin or document.origin.contextBridge in Electron 12 (replacing the older contextBridge-less preload approach). Electron 20+ added exposeInIsolatedWorld for isolated contexts but did not change structured-clone semantics or add origin gating. No pending Electron fixes or RFCs aim to add automatic prototype preservation or origin-based permissions; maintainers consider manual origin checks the correct pattern.| Confirmed (documented/observable) | Likely Explanation (inferred) |
|---|---|
| Structured clone strips prototypes, getters/setters, non-enumerable properties | Security boundary: prevents prototype pollution and ensures only serializable data crosses contexts |
Class instances become plain objects; instanceof fails | Intentional design — functions and symbols cannot be cloned |
| No built-in origin gating; any renderer in session can call exposed APIs | Electron's threat model treats the renderer process as the trust boundary, not individual origins |
exposeInIsolatedWorld (Electron 20+) changes target world only | Does not mitigate prototype loss or add permissions |
// preload.js
const { contextBridge } = require('electron');
class ApiService {
constructor() { this.state = 'ready'; }
doWork(payload) { return `processed: ${payload}`; }
getState() { return this.state; }
}
const instance = new ApiService();
// Expose bound methods — retains this but not prototype
contextBridge.exposeInMainWorld('api', {
doWork: instance.doWork.bind(instance),
getState: instance.getState.bind(instance)
});
Renderer receives { doWork: f, getState: f } — plain functions bound to the preload instance. No instanceof ApiService, but methods work.
// preload.js
const ALLOWED_ORIGIN = 'https://your-app.example.com';
contextBridge.exposeInMainWorld('api', {
doWork(payload) {
// This runs in the renderer context — window is the renderer's window
if (window.location.origin !== ALLOWED_ORIGIN) {
throw new Error('Origin not allowed');
}
return instance.doWork(payload);
}
});
Caveat: A compromised renderer can spoof window.location.origin. Treat this as defense-in-depth, not a security boundary.
// preload.js
const methods = {
doWork(payload) { return `processed: ${payload}`; },
getState() { return instance.state; }
};
contextBridge.exposeInMainWorld('api', {
invoke(method, ...args) {
if (window.location.origin !== ALLOWED_ORIGIN) throw new Error('Origin not allowed');
if (!(method in methods)) throw new Error(`Unknown method: ${method}`);
return methods[method](...args);
}
});
Single entry point, easier to maintain, centralizes origin check.
console.log(typeof window.api.doWork, window.api instanceof ApiService, Object.getPrototypeOf(window.api)) — observe function, false, Object.prototype.file://, http://localhost, https://example.com and call window.api.doWork('test') — confirm any origin can invoke without the manual check.Are you exposing a stateful singleton (methods mutate internal state) or a stateless service (methods are pure or read-only)? This changes whether bound-method exposure suffices or you need a factory/reconstruction pattern.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.