NW.js nodeIntegration: When to Enable It and How to Replace It Safely
Enabling nodeIntegration in NW.js gives renderers direct Node.js access, but it also removes the sandbox. Here's how to decide and how to replace it with a preload script.
29 Aug 2025, 07:43 UTC

Your NW.js app needs to read a local config file from the renderer. The quickest fix is setting node-integration to true in the manifest. That gives the renderer direct access to require('fs') and the rest of Node. It also removes the Chromium sandbox from that renderer. If the renderer ever loads a page with an injected script, that script can read files, spawn processes, or exfiltrate data.
The practical rule: keep node integration off for any window that might display untrusted content. Expose only the specific operations the renderer needs through a preload script. Enable node integration only for fully trusted, offline tools, and even then audit the renderer code.
What enabling nodeIntegration actually changes
In NW.js, node-integration controls whether the web context can use Node.js APIs. When it is on, the renderer gets require, process, and other Node globals. That is convenient for desktop-style apps that treat the renderer like a local script.
The trade-off is attack surface. A cross-site scripting (XSS) flaw in the renderer becomes remote code execution (RCE) on the user's machine. The renderer is no longer sandboxed from the operating system. The research brief for this topic notes that NW.js documentation recommends disabling node integration by default and using the node-preload option for required modules. Treat that as a version-dependent recommendation and check your NW.js release notes.
The safer pattern: preload plus context isolation
A preload script runs before the renderer page loads. It can use Node APIs, but it should expose a narrow, whitelisted interface to the page. The renderer then calls that interface instead of calling Node directly.
The manifest fields below are illustrative. Field names and availability vary by NW.js version. Verify them against the documentation for the version you ship.
{
"name": "safe-reader",
"main": "index.html",
"node-integration": false,
"node-preload": "preload.js",
"context-isolation": true
}
If your version supports context isolation, use its bridge mechanism to pass values between the preload context and the page. The exact API differs; do not assume a global assignment works across isolated contexts without checking.
A minimal file-read API
The preload script below limits reads to a single directory. It is a shape, not a tested implementation.
const fs = require('fs');
const path = require('path');
const ALLOWED_DIR = path.join(__dirname, 'data');
function readConfig(filename) {
const fullPath = path.resolve(ALLOWED_DIR, filename);
if (!fullPath.startsWith(ALLOWED_DIR + path.sep)) {
throw new Error('Access denied');
}
return fs.readFileSync(fullPath, 'utf8');
}
// Expose only this function. The bridge mechanism depends on your NW.js version.
window.safeReader = { readConfig };
In the renderer, call window.safeReader.readConfig('app.json') instead of require('fs').readFileSync(...). The renderer never sees fs, path, or process.
Trade-offs and limitations
Preload scripts add indirection. Every new capability needs a deliberate export, which slows down quick prototypes. That friction is the point: it forces you to decide what the renderer can do.
Context isolation is not a silver bullet. The preload script still runs with Node access, so a bug there can expose more than intended. You still need a Content Security Policy (CSP), input validation, and a review of any third-party code that runs in the renderer.
Some legacy libraries assume Node globals in the renderer. If you cannot remove that assumption, isolate the window from untrusted content, disable remote page loading, and audit every renderer dependency.
| Approach | Renderer access | Attack surface | Best for |
|---|---|---|---|
node-integration: true | Full Node.js | High if renderer loads untrusted content | Trusted internal tools with no remote content |
node-integration: false + preload | Only whitelisted functions | Lower, but depends on preload quality | Most apps that need a few Node capabilities |
| No Node access at all | None | Lowest | Pure web UI with a separate backend process |
How to verify the change
Run your app and open the renderer's developer tools. These checks are diagnostic, not proof of security.
- Type
typeof requirein the console. With node integration off, the expected result is"undefined". If it returns"function", node integration is still on. - Type
typeof window.safeReader. The expected result is"object"if the preload exposed the API. If it is"undefined", the preload did not load or the bridge failed. - Call
window.safeReader.readConfig('app.json')with a file that exists in the allowed directory. The expected result is the file contents. Then try a path like../secret.txt; the expected result is an error, not file contents. - Inject a script tag that tries
require('child_process').exec('calc'). With node integration off, the expected result is aReferenceError. If it launches a process, the renderer is not sandboxed.
If a check fails, compare your manifest against the NW.js documentation for your version. Field names such as node-preload and context-isolation may differ or be unavailable in older releases.
Rollback and closing
Changing node-integration and adding a preload changes app state. To roll back, restore the previous manifest and remove the preload file, then restart the app. Keep the old manifest in version control so you can compare.
Start with node integration off. Add a preload API for each capability the renderer actually needs. If you must enable node integration, document why, restrict the renderer to trusted content, and audit the renderer code for injection vectors. The convenience is real, but so is the blast radius.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.