This code "resolved path" is still running outside of node_modules. It is using the same underlying mechanism as the other approaches you used. Archives and Symlinks are evil anyways. Sentinel will block both soon
I learned about cache poisoning and Im already working on a solution to fully isolate node cached dependencies. Node A wont be able to alter Node B required modules.
I think im going to force every package installed in a nodered instance to be bundled with their dependencies before loaded and executed by nodered. Then block their access to require. I have already blocked their access to deleting and set require cached dependencies, but it is still possible to change shared dependencies variables that libraries use to control their state. I think this is the only way to really lock the global require. Then move all to a read only (nodered user) dir where nodered picks nodes, to not let any nodes/plugins change other modules source files.
dynamic imports/require of files outside of the package won't be allowed (using real file path to resolve symlinks!). They suck and it can be exploited. Bundlers dont like them too.
First iteration of Sentinel's Palette Manager
POST PUT DELETE /nodes core routes are blocked and editorTheme.palette.editable is set to false.
Now, usrDir/node_modules is set to read-only for the Node-RED runtime user. This effectively prevents any code executed within Node-RED from mutating dependencies, eliminating the risk of cross-node contamination or corruption after a restart.
On top of that, all installed packages are statically bundled during installation, with explicit exclusions for dynamic imports/requires, native .node addons, WebAssembly modules, and Node-RED–specific assets that are copied over to the resulted bundle.
Combined with the new Sentinel guardrails, this solution ensures that any attempt by a package to tamper with the require cache at runtime is immediately detected and blocked, maintaining strict runtime integrity.
Will release all the new changes soon.
There’s an important risk worth highlighting around how flows are executed in environments where dependencies (like node_modules) can be modified.
If the runtime environment is mutable, then the behavior of your flows is effectively mutable as well. In practice, most teams don’t have the bandwidth to audit the full source code of every dependency they install—especially when many packages are large, bundled, or obfuscated into single files.
This opens the door to subtle issues, such as dynamic require patterns that can alter runtime or editor behavior based on environment heuristics (for example, detecting production conditions). These changes can be difficult to detect and even harder to trace back once they’re in place.
This is exactly the class of problem Sentinel is designed to address—by preventing untrusted or potentially harmful dependencies from entering and affecting your Node-RED environment in the first place.
Sentinel Admin Portal Preview
This is a preview of Sentinel’s upcoming Admin Portal. It provides a centralized interface where administrators can install packages and manage permissions, including fine-grained node-level grants.
Access to the portal is streamlined—admins can simply click the Sentinel logo. From there, they’ll also get real-time visibility into security activity, such as attempted threats or unauthorized actions within their environment.
Why move it out of the editor?
This is a deliberate architectural decision:
- Security isolation: Keeping Sentinel outside the editor reduces the attack surface and limits exposure to potentially malicious plugins.
- Operational clarity: A dedicated interface enables a broader, more scalable view of system state, permissions, and security insights.
- Better UX for admins: Moving beyond a constrained side panel allows for richer data visualization and more effective management workflows.
In short, this shift separates concerns—protecting the control plane while improving usability and visibility.
Package Management Model (Sentinel vs Native)
Sentinel also replaces the native package manager with its own controlled installation flow. Instead of installing packages directly into a shared dependency tree, Sentinel bundles each package together with its dependencies (excluding native modules) into an isolated unit.
This design directly addresses common attack vectors like require pollution. At runtime, the preloader enforces strict boundaries: it guards against tampering with the require cache (both set and delete operations) and restricts packages to only load files within their own directory scope.
By combining controlled bundling with runtime enforcement, Sentinel ensures that dependencies remain isolated and that deep object properties or shared functions cannot be silently overridden by other packages. In practice, this creates a hardened execution model where packages operate in well-defined boundaries instead of a mutable global environment.
When installed, sentinel disables the native palette manager automatically for all Users.
Oh I'm also creating another step in the installation process to analyse the bundled code to detect dynamic imports/requires that esbuild can't resolve statically. All packages that contain these won't be registered into the runtime unless an admin accepts some sort of warning message that will be displayed in the installation page.
Bundling to cjs ensures that all dependencies, besides dynamic ones, pass through the loader guards. Some libraries unfortunatelly break when doing this because they were written using esm only features. For those edge cases, admins will be able to choose to bypass the bundling phase upon accepting the risks.
I'm going to investigate how to create a custom esm loader as well for these esm only packages.
I think I found a way to bundle to esm while keeping entrypoints in cjs. I have to try that. The reason for bundling to esm is because esm to cjs is more compatible then cjs to esm, and a lot of new libraries are being written to esm only, like aws sdk v3. Hopefully it wont be a lot of rework
Yeah this is going to work and it will make nrg v3 builds to esm! (Kind of haha)
Decided to use rolldown (or rollup) instead of esbuild because the conversion from cjs to esm isn’t well supported in esbuild. Rolldown is compatible to vite/rollup plugins, it is way faster and was created to replace rollup+esbuild in vite builds.
The new Package Installer is working as intended. In this example, node-red-contrib-moment attempted to access process.env, but the request was blocked because no permissions were granted.
This case is harmless, but it highlights a critical risk. If a package—or one of its dependencies—tries to read sensitive data from process.env, it could exfiltrate credentials during installation without the user ever noticing. In reality, no one audits 100% of their dependency tree.
Sentinel addresses this by defaulting to a deny-all model. Nothing gets access unless explicitly permitted, reducing the attack surface and protecting sensitive data from unauthorized access.
4 Apr 23:56:23 - [info] Starting flows
4 Apr 23:56:23 - [info] Started flows
[@allanoricil/nrg-sentinel] BLOCKED process.env.OS_LOCALE_S_DEBUG — node-red-contrib-moment lacks process:env:read
Call stack:
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5053:34
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5201:14
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5267:23
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:6:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_ALL — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5150:50)
at detectorBase (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5233:28)
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5269:30
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:6:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_MESSAGES — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5150:64)
at detectorBase (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5233:28)
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5269:30
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:6:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANG — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5150:83)
at detectorBase (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5233:28)
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5269:30
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:6:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANGUAGE — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5150:95)
at detectorBase (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5233:28)
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5269:30
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:3:48
at file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:6:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED RED.nodes.registerType("moment") — node-red-contrib-moment lacks registry:register
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.mjs:5484:13)
at /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:10:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["registry:register"] } } }
[@allanoricil/nrg-sentinel] BLOCKED RED.nodes.registerType("humanizer") — node-red-contrib-moment lacks registry:register
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/humanizer.mjs:4504:13)
at /private/tmp/sentinel-dev-U239Wj/node_modules/node-red-contrib-moment/_sentinel/humanizer.cjs:10:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["registry:register"] } } }
4 Apr 23:57:44 - [error] [nrg-sentinel] Failed to load ESM bundle moment: [@allanoricil/nrg-sentinel] registerType("moment") blocked — needs registry:register. Caller: node-red-contrib-moment
4 Apr 23:57:44 - [error] [nrg-sentinel] Failed to load ESM bundle humanizer: [@allanoricil/nrg-sentinel] registerType("humanizer") blocked — needs registry:register. Caller: node-red-contrib-moment
You can also see that the package was properly bundled with its whole dependency tree during installation.
Here are the logs showing what happens after every grant is given to a package. You will notice error messages keep disapearing as soon as grants are given.
5 Apr 00:49:42 - [info] Starting flows
5 Apr 00:49:42 - [info] Started flows
5 Apr 00:49:42 - [info] Server now running at http://127.0.0.1:1880/
[@allanoricil/nrg-sentinel] BLOCKED process.env.OS_LOCALE_S_DEBUG — node-red-contrib-moment lacks process:env:read
Call stack:
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5053:34
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5201:14
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5267:23
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_ALL — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5150:50)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_MESSAGES — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5150:64)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANG — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5150:83)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANGUAGE — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5150:95)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED RED.nodes.registerType("moment") — node-red-contrib-moment lacks registry:register
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361069422:5484:13)
at /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:11:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["registry:register"] } } }
[@allanoricil/nrg-sentinel] BLOCKED RED.nodes.registerType("humanizer") — node-red-contrib-moment lacks registry:register
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/humanizer.mjs?v=1775361069426:4504:13)
at /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/humanizer.cjs:11:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["registry:register"] } } }
5 Apr 00:51:09 - [error] [nrg-sentinel] Failed to load ESM bundle moment: [@allanoricil/nrg-sentinel] registerType("moment") blocked — needs registry:register. Caller: node-red-contrib-moment
5 Apr 00:51:09 - [error] [nrg-sentinel] Failed to load ESM bundle humanizer: [@allanoricil/nrg-sentinel] registerType("humanizer") blocked — needs registry:register. Caller: node-red-contrib-moment
5 Apr 00:51:09 - [info] [@allanoricil/nrg-sentinel] Hot-loaded: node-red-contrib-moment
5 Apr 00:51:19 - [info] [@allanoricil/nrg-sentinel] Hot-unloaded: node-red-contrib-moment
5 Apr 00:51:19 - [info] [@allanoricil/nrg-sentinel] Package grant saved (SPA): node-red-contrib-moment
[@allanoricil/nrg-sentinel] BLOCKED process.env.OS_LOCALE_S_DEBUG — node-red-contrib-moment lacks process:env:read
Call stack:
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5053:34
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5201:14
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5267:23
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_ALL — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5150:50)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_MESSAGES — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5150:64)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANG — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5150:83)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANGUAGE — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5150:95)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED httpAdmin.get() — node-red-contrib-moment lacks http:admin
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5485:17)
at /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:11:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["http:admin"] } } }
[@allanoricil/nrg-sentinel] BLOCKED httpAdmin.get() — node-red-contrib-moment lacks http:admin
Call stack:
at module.exports (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361079464:5494:17)
at /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:11:26
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["http:admin"] } } }
5 Apr 00:51:19 - [info] [@allanoricil/nrg-sentinel] Hot-loaded: node-red-contrib-moment
5 Apr 00:51:19 - [info] [@allanoricil/nrg-sentinel] Reloaded after grant change: node-red-contrib-moment
5 Apr 00:51:33 - [info] [@allanoricil/nrg-sentinel] Hot-unloaded: node-red-contrib-moment
5 Apr 00:51:33 - [info] [@allanoricil/nrg-sentinel] Package grant saved (SPA): node-red-contrib-moment
[@allanoricil/nrg-sentinel] BLOCKED process.env.OS_LOCALE_S_DEBUG — node-red-contrib-moment lacks process:env:read
Call stack:
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5053:34
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5201:14
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5267:23
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_ALL — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5150:50)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LC_MESSAGES — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5150:64)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANG — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5150:83)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
[@allanoricil/nrg-sentinel] BLOCKED process.env.LANGUAGE — node-red-contrib-moment lacks process:env:read
Call stack:
at getEnvLocale (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5150:95)
at detectorBase (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5227:22)
at detector.sync (file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5233:28)
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5269:30
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:3:48
at file:///private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.mjs?v=1775361093944:5501:16
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async /private/tmp/sentinel-dev-0e0XQP/node_modules/node-red-contrib-moment/_sentinel/moment.cjs:7:21
To allow, add to settings.js:
sentinel: { packages: { "node-red-contrib-moment": { capabilities: ["process:env:read"] } } }
5 Apr 00:51:34 - [info] [@allanoricil/nrg-sentinel] Hot-loaded: node-red-contrib-moment
5 Apr 00:51:34 - [info] [@allanoricil/nrg-sentinel] Reloaded after grant change: node-red-contrib-moment
5 Apr 00:52:31 - [info] [@allanoricil/nrg-sentinel] Hot-unloaded: node-red-contrib-moment
5 Apr 00:52:32 - [info] [@allanoricil/nrg-sentinel] Package grant saved (SPA): node-red-contrib-moment
5 Apr 00:52:32 - [info] [@allanoricil/nrg-sentinel] Hot-loaded: node-red-contrib-moment
5 Apr 00:52:32 - [info] [@allanoricil/nrg-sentinel] Reloaded after grant change: node-red-contrib-moment
When a package is installed, a static analysis is performed immediately after bundling the server-side code to identify the permissions it may require.
Going forward, this analysis will be compared against the capabilities declared by the package author to ensure alignment and detect potential discrepancies.
Packages that do not comply with NRG Sentinel analysis or fail to declare the required grants will be assigned a reduced trust score, reflecting lower confidence in their integrity and transparency.
When installing an untrusted package, a manual review must be completed before the package can be activated. Static analysis is provided only as a support tool to highlight areas that may require closer inspection.
If the administrator trusts the package author, they may choose to install the package as trusted. In this mode, the package behaves similarly to a Node-RED core package.
However, this introduces significant risk: all dependencies of the package will bypass security controls, increasing exposure to supply chain attacks. For example, an indirect dependency could extract environment variables, inject hooks to monitor message flows, or rewrite nodes through virtual routes without the user’s awareness.
Trusted vs. Untrusted Packages (Filesystem Model)
Packages are classified based on their installation location and the level of runtime enforcement applied to them.
Untrusted packages are installed under usrDir/node_modules. These packages are subject to all runtime guards enforced by NRG Sentinel.
Trusted packages are installed in a separate, dedicated directory. Both trusted and untrusted directories are mounted as read-only for the Node-RED runtime user, preventing packages from mutating their own source code or that of others during execution.
The key distinction lies in enforcement: trusted packages bypass all runtime guards applied by NRG Sentinel and effectively operate with the same level of privilege as core packages.
This model introduces a clear trust boundary at the filesystem level while preserving runtime integrity through immutability and controlled execution paths.
Trusted
Packages operate with full privileges, bypassing all runtime guards and behaving equivalently to core Node-RED components.
Needs Review
Packages require explicit administrative approval. The admin reviews the entire bundled codebase, enabling inspection of both direct and indirect dependencies that may be compromised before granting permissions.
Guarded
Packages have been reviewed, approved, and installed. They operate under enforced runtime guards with permissions (grants) explicitly applied and constrained according to the review outcome.
Added vulnerabilities info from Snyk, Socket and os.dev in the package details and in the package review pages. Be aware that these major apps scanners do not capture vulnerabitlities that happen specifically in Node-RED environments because they are mostly runtime ones operating under the rules of the game.
With NRG Sentinel, for example, your environment variables are guarded against evil packages.
Or a more advanced one, like dynamic routing of messages (virtual wiring)...
Some packages come from well-known vendors—such as @flowfuse/node-red-dashboard—and can be installed as “trusted.” When marked this way, Sentinel disables all runtime guards for those packages.
That said, this introduces a non-trivial risk. If any direct or indirect dependency within that package is compromised, Sentinel will not intercept it. Trusting a package effectively extends trust to its entire dependency tree—and in practice, most vendors do not fully audit their transitive dependencies. This creates a cascading trust model, which is exactly how supply chain attacks propagate.
The practical takeaway: treat “trusted” as an exception, not the default.
A more resilient approach is to install packages with Sentinel protections enabled, allow them to fail under guard enforcement, and then incrementally grant permissions as needed. This forces visibility into actual runtime behavior and ensures you are validating the entire bundle—not just the vendor’s surface-level code.
Sentinel’s bundling step during installation is designed to handle even complex packages, such as @flowfuse/node-red-dashboard (Dashboard 2.0).
It produces a CommonJS (CJS) entry point that encapsulates and loads the entire ES module (ESM) bundle. This ensures compatibility with the runtime while still allowing Sentinel to analyze and instrument the full dependency graph as a single, controlled unit.
































