Safe Node Access in NW.js: Preload Scripts Over Full Renderer Integration
Enable Node integration in NW.js without exposing the full runtime to your renderer by combining context isolation with a preload-based API surface.
09 Oct 2025, 07:16 UTC

You need file access, but not full Node in every script
You are building a desktop application with NW.js, and the renderer needs to read a configuration file or save user data. The fastest fix looks like setting nodeIntegration: true in package.json. That gives your renderer direct access to fs, child_process, and every other Node module. It also gives any script that runs in that window - including third-party libraries or content you did not write - the same access. A single cross-site scripting flaw can become arbitrary code execution on the user's machine.
The useful takeaway is that you can keep Node integration enabled while isolating the renderer. NW.js supports running a preload script in a privileged context and exposing only the functions you choose to the page. This keeps file operations available where you need them without handing the entire Node runtime to untrusted code.
Why enabling nodeIntegration without isolation is risky
nodeIntegration controls whether Node modules are available in the renderer process. When it is true, code in your window can require('fs') or require('child_process') directly. contextIsolation separates the preload script's JavaScript environment from the page's environment. If you enable integration but leave isolation off, the page and the preload share the same global object, so anything the page can reach, the preload can reach - and vice versa.
That means an injected script can grab process or require from the global scope and run shell commands. Keeping contextIsolation: true means the preload runs in its own context, and the page only sees what you explicitly pass across.
Worked example: a narrow read-only file API
This example assumes a supported NW.js version where preload scripts and context isolation are available. Verify your version before copying the configuration into production.
1. Configure the window in package.json
{
"name": "safe-node-example",
"main": "index.html",
"window": {
"nodeIntegration": true,
"contextIsolation": true,
"preload": "preload.js"
}
}
Edit package.json and create preload.js in your project directory. You need read/write access to the project files. A typo in the window flags can silently fall back to defaults, so check the developer console after launch.
2. Create preload.js
The preload script runs before the page loads and can use Node's fs module. It exposes only one function to the page. The exact method for crossing the isolation boundary varies by NW.js version; consult your release notes for the current preload bridge.
const fs = require('fs');
const safeApi = {
readText: (filePath) => {
if (typeof filePath !== 'string') {
throw new TypeError('filePath must be a string');
}
return fs.readFileSync(filePath, 'utf8');
}
};
// Expose safeApi to the renderer using your NW.js version's preload bridge.
// Example: window.fileApi = safeApi; (mechanism varies by version)
3. Call it from the renderer
// index.html renderer script
function loadConfig() {
try {
const contents = window.fileApi.readText('./config.json');
console.log(contents);
} catch (err) {
console.error('Read failed:', err.message);
}
}
The renderer never imports fs itself. If an attacker injects a script into the page, it can call window.fileApi.readText, but it cannot call require('fs') or process.exit directly.
Verify the boundary
- Launch the app with the configuration above.
- Open DevTools and confirm
window.fileApiexists whilewindow.requireis undefined. - Load a test HTML snippet that attempts
require('child_process'); it should throw or return undefined. - Confirm
fileApi.readTextreturns the expected file contents.
If window.require is available, context isolation is not working as expected, and the preload boundary is not enforced.
Trade-offs and limitations
| nodeIntegration | contextIsolation | Renderer can require Node | Preload can require Node | Relative risk |
|---|---|---|---|---|
| true | false | Yes | Yes | High |
| true | true | No | Yes | Medium |
| false | true | No | No | Low |
- Boilerplate: Every Node capability you need must be explicitly wrapped and exposed. Direct
fsaccess in the renderer is simpler but less safe. - Version dependency: Preload and isolation behavior varies across NW.js releases. Older versions may not support the same bridge APIs.
- API surface is still attack surface: Even a narrow function like
readTextcan be abused if it accepts arbitrary paths. Validate inputs inside the preload, not in the renderer.
Closing checklist
- Enable
nodeIntegrationonly for windows that truly need Node access. - Keep
contextIsolation: true. - Expose functions through a preload script, never the whole
requireobject. - Validate all arguments inside the preload before calling Node APIs.
- Test with untrusted content to confirm Node modules are unreachable from the page.
This pattern gives you local file access where your app needs it, without making every renderer script a potential entry point to the user's operating system.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.