Skip to main content

The core constraint

Sentinel runs inside the same Node.js process as every third-party node package it protects against. There is no sandbox boundary, no separate process, and no OS-level isolation — everything happens in one heap. Meaningful enforcement in that environment requires layered hardening techniques, each closing a bypass that would exist if only the others were active.

The five layers

LayerTechniqueWhat it closes
0 — Prototype hardeningObject.preventExtensions on all built-in prototypesPrototype pollution before any third-party code runs
1 — Module interceptionModule._load hook + non-configurable lockrequire() of fs, http, child_process, vm, worker_threads
2 — Node isolationES6 Proxy on every getNode() return valueProperty reads, writes, and defineProperty on live node instances
3 — Surface hardeningGuarded Express routing, process.env Proxy, router-stack ProxyPost-init manipulation of the HTTP server and environment
4 — Network policyOutbound HTTP/HTTPS/socket allowlistExfiltration paths not covered by the module gate
Cross-cuttingIntrinsic capture, call-stack introspection, file integrity watchdogPrototype mutation of guard helpers, call-identity forgery, on-disk tampering

Intrinsic capture — a prerequisite for every other layer

Before Layer 0 runs, every built-in prototype method that guard logic depends on is pinned as a standalone bound function:
var $strIncludes = Function.prototype.call.bind(String.prototype.includes);
var $arrSlice    = Function.prototype.call.bind(Array.prototype.slice);
// … and so on for every method the guards use …
This is not a numbered layer — it is a prerequisite. A malicious package that runs later and overwrites String.prototype.includes cannot blind the stack-frame checks, because the guard logic uses the captured alias, not the live prototype property.
If intrinsic capture were skipped, a package loaded after the preload could overwrite any prototype method that Sentinel’s guards call, silently disabling all downstream protection.

How the layers compose

Each layer closes a bypass that would exist without it:
Attacker actionBlocked by
Overwrite String.prototype.includes to blind stack checksIntrinsic capture
Inject Object.prototype.admin = true to forge capability grantsPrototype hardening
Call require('fs') directlyModule interception
Overwrite Module._load to strip the hookModule._load non-configurable lock
Call Object.defineProperty(node, 'credentials', getter) to steal credentialsNode Proxy defineProperty trap
Splice a route handler into app._router.stackRouter-stack Proxy
Replace app._router with an unguarded objectconfigurable: false on _router
Edit preload.js on disk to remove guardsFile integrity watchdog
Read process.env.NODE_RED_CREDENTIAL_SECRETprocess.env Proxy
Call vm.runInNewContext(malicious_code)require('vm') blocked at Module._load

Technique pages

Prototype hardening

How Object.preventExtensions blocks prototype pollution before any third-party code runs.

Module interception

How the Module._load hook gates dangerous built-in modules and locks itself against removal.

Node isolation

How every getNode() return value is wrapped in an ES6 Proxy enforcing the dual-axis capability check.

Surface hardening

How Sentinel guards the Express server, process.env, and the router stack after Node-RED initialises.

Network policy

How Sentinel enforces the URL allowlist for outbound HTTP/HTTPS calls.

Attack scenarios

The 34 attack scenarios that Sentinel detects and blocks in E2E testing.

Build docs developers (and LLMs) love