Preload and IPC
Why the renderer is isolated
Understand why Node.js stays out of the page and why a preload bridge exposes only deliberate desktop capabilities.
JavaScript in the browser is limited, for good reasons. A web page cannot read files on your disk or launch programs. If a page gets compromised, through an injected script or a bad dependency, the damage stays inside the tab.
Electron gives you desktop power. The question is who gets it. My answer, and Electron’s, is that page code should not receive all of it.
Three protections
We set these in the previous module. Let’s be clear about what each one does:
nodeIntegration: falseremoves direct Node.js access from the page. Norequire('fs')in renderer code.contextIsolation: truegives the preload script and the page separate globals. The page cannot reach into the preload’s variables.sandbox: trueruns the renderer in Chromium’s sandbox, so even a full renderer compromise has limited OS access.
With all three on, a compromised page can only do what your preload bridge lets it do. That makes the bridge the most important security surface in the app.
The preload is privileged
Even in a sandboxed renderer, the preload script can do more than the page. It can talk to the main process through ipcRenderer. Whatever it exposes through contextBridge becomes available to every script running in the page, including one you didn’t write.
Treat every method you expose as a permanent public API. Adding one is easy. Taking one away later, once code depends on it, is not.
A bridge that is too powerful
This looks convenient, and I see it in many tutorials:
contextBridge.exposeInMainWorld('electron', {
send: (channel, value) => ipcRenderer.send(channel, value)
})
The problem is that any renderer code can pick any channel. Today you have two handlers in the main process. Next year someone adds a files:delete handler for an internal feature. The bridge didn’t change, but now the page can call it.
One method per intention
Expose a method for each thing the interface is allowed to do, and nothing else:
contextBridge.exposeInMainWorld('notesAPI', {
saveNotes: notes => ipcRenderer.invoke('notes:save', notes)
})
The page can save notes. It cannot pick a channel. When a new handler appears in the main process, the page can’t reach it until you deliberately add a method here.
Be careful, though: this does not make notes trusted. The main process still has to check who sent the message and validate the value before writing anything. We’ll do that in the next lessons.
Here is an exercise I find useful. Imagine an attacker’s script is running inside your renderer. Write down everything it could do through window.notesAPI. For Desktop Notes the list should be four short lines, and none of them more powerful than the interface needs. If the list is long or vague, the bridge is too wide.
Lesson completed