Why Electron Apps Need a Sandbox
Electron apps occupy a unique position in the security landscape. They combine standard web content rendered by Chromium with privileged JavaScript that can interact with the operating system through Node. This marriage enables the rich, native-feeling functionality users expect from desktop applications. But it also means any vulnerability that gives an attacker control over your web content—an XSS flaw, a malicious image, or a crafted JSON payload—can be escalated into arbitrary code execution with full system access.
The traditional response to this threat is to eliminate the vulnerabilities themselves. You might think that disabling Node integration or carefully sanitizing inputs is enough. But as any security researcher will tell you, the attack surface is vast and ever-changing. Even seemingly innocent operations like displaying an image or parsing JSON have historically led to remote code execution. The more prudent approach is to assume a worst-case scenario and design for containment rather than prevention alone.
Nothing is secure. Security is not an on or off thing. Everything has bugs. Virtual machines have bugs, kernels have bugs, hardware has bugs. We really need to be thinking about risk management, ways that we can account for the fact that there are going to be bugs and make sure that they have minimum impact.
This is the philosophy behind Chromium's multi-process architecture, which was designed from the outset to treat web content as untrusted and constrain what it can do. Electron inherits this capability through its sandbox option under webPreferences. Flipping this switch fundamentally changes the assumptions your renderer processes operate under: they no longer have access to Node or its core modules like fs, crypto, or child_process. The available Electron surface area shrinks dramatically, leaving renderer processes with little more than the ability to send messages to the main process.
Making that transition in an existing Electron app is not straightforward, but it is possible without sacrificing functionality. The key is reorganizing your code and building new bridges between processes.
Untangling Renderer Dependencies
The first obstacle in moving to a sandboxed architecture is often code organization. When you've shared a common utilities folder between your main and renderer processes, that folder tends to accumulate a mix of genuinely reusable functions and code that depends on Node's filesystem or process capabilities. Problems arise when renderer code imports a file to access one utility and accidentally pulls in a Node dependency as a side effect. The import chain becomes a web of interconnected bundles that will break the moment the sandbox cuts off Node access.
For developers facing this problem, dependency-cruiser is an invaluable tool. It visualizes import chains as graphs, allowing you to see exactly how a renderer-side component ends up referencing fs or path through a chain of interdependent files. When the graph becomes too large, options like exclude, focus, and doNotFollow help you prune it down to something legible and actionable.

Once you understand the dependency chains, you can begin restructuring. Dependency-cruiser's validation rules act as a linter for code organization, preventing future violations by blocking imports that cross the main/renderer/preload boundaries. You can even enforce a rule that blocks any Node import in renderer code entirely:
{
name: 'no-node-in-renderer',
comment: 'The renderer process should not use Node built-ins',
severity: 'error',
from: {
path: '^src/(renderer|preload|common)',
},
to: {
dependencyTypes: ['core']
}
}
An unexpected payoff of this cleanup is smaller JavaScript bundles. The preload script, which runs on every page navigation, benefits particularly from shedding unnecessary code. After the reorganization, your folder structure starts to accurately reflect the actual webpack bundles produced at build time, making it clear which code belongs to which process.
Communicating Across the Boundary
For Electron apps that embed web content, the preload script is the standard mechanism for exposing desktop functionality to the page. But enabling the sandbox—and its companion security measure, context isolation—creates a new problem: how do you share objects between the preload script and the guest page when they operate in completely isolated JavaScript contexts?
This isolation is by design. Content scripts in Chromium run in their own execution environments, so a compromised page can't redefine globals like JSON.parse within a trusted script's scope. But it also means you can't simply assign an API object to window in the preload and expect the page to see it.
Electron's answer is the contextBridge module. It creates a controlled, two-way channel between isolated worlds in the form of a global object exposed to the page. The pattern looks like this:

Notice the architectural shifts this introduces:
- The preload uses
contextBridgeto create a global in the page context instead of assigning directly towindow. - In a sandboxed renderer, the preload's only capability is to send messages to the main process — it cannot execute Node code directly.
- The main process becomes the gatekeeper, responsible for validating incoming messages and filtering out anything harmful or unspecified.
- If the main process handler returns a
Promise, that promise is marshaled across both the IPC boundary and the context bridge, soawaiting the in-page call provides real completion semantics in the main process.
The functional result is that everything your app could do before enabling the sandbox remains possible. It just takes a few more hops through well-defined, validated channels. The preload script no longer performs operations directly; it brokers requests to the main process, which acts as the single trust boundary for privileged actions.
The effort required to sandbox an Electron app fights against years of accumulated infrastructure. Untangling dependency graphs, restructuring import paths, and rebuilding IPC bridges is a significant engineering investment. The payoff is an architecture that treats web content as permanently compromised and outlines what it can do, rather than hoping no attacker ever finds a way in.



