Repository navigation
externalModules: version-scoped allowList/denyList entries enforced inconsistently — deny-listing one version makes a module unusable in function nodes #6011
Description
Current Behavior
externalModules.modules.allowList/denyList entries can be version-scoped (module@version), and those versions are evaluated with semver.satisfies (parseModuleList / checkAgainstList in registry/lib/util.js). But the three call sites that enforce the lists disagree on which version to check, and the version that actually gets loaded (the installed one) is never validated:
| Call site | Location | Version passed to checkModuleAllowed |
|---|---|---|
checkFlowDependencies (deploy, already-installed modules) |
externalModules.js:303 | the version declared in the flow (foo@1.0.0 → 1.0.0; plain foo → undefined) |
installModule (deploy, not-yet-installed modules) |
externalModules.js:343 | the declared version |
requireModule / importModule (function node startup) |
externalModules.js:87 / :108 | null — and the raw spec string ("foo@2.4.0") is tested against module-name rules like ^foo$, so those rules can never match a versioned spec |
Consequences (all verified by execution, see below):
-
One deny-listed version makes the module unusable in function nodes. With
denyList: ["foo@2.3.0"](block only a known-bad version) andfoo@2.4.0installed:- a module ref without version (
foo) is rejected at deploy withmodule_not_allowed, although 2.4.0 does not match the denied2.3.0.checkFlowDependenciespassesundefinedas the version, andcheckAgainstList(util.js:140-147) matches a version-less request against any rule — including version-scoped ones:if (version && rule.version) { if (semver.satisfies(version,rule.version)) { return rule; } } else { return rule; // version-less request: even a `foo@2.3.0` rule matches any `foo` }
- a versioned ref (
foo@2.4.0) passes the deploy check correctly, but the node then fails at startup:importModulenever matches its raw"foo@2.4.0"string against^foo$rules, and the import dies on the separate, still-open "Error: Cannot find module" when specifyingfunctionnode module version #4541 resolve problem.
So once any version-scoped deny entry exists for a module's name, there is no working way to use that module in a function node — including for versions the admin explicitly did not deny.
- a module ref without version (
-
Deny rules are checked against the declared version, not the installed one.
denyList: ["foo@2.x"], installedfoo@2.4.0, flow declaresfoo@1.0.0→ deploy check passes, because the check evaluates the declared1.0.0even though the module loaded into the sandbox would be the installed 2.4.0. -
Symmetric false rejection with a version-scoped allowList.
allowList: ["foo@2.x"], installedfoo@2.4.0, flow declaresfoo@1.2.0→ deploy rejected withmodule_not_allowed, even though the module that would actually be loaded is the installed, allow-listed 2.4.0.
Steps to Reproduce
I extracted parseModuleName, isInstalled, checkFlowDependencies, importModule, requireModule (externalModules.js) and parseModuleList, checkAgainstList, checkModuleAllowed (util.js) verbatim from current main and ran them against a scratch userDir with foo@2.4.0 installed:
== denyList ['foo@2.3.0'], installed foo 2.4.0 ==
admin intent: deny only 2.3.0. semver.satisfies('2.3.0','2.3.0') = true | semver.satisfies('2.4.0','2.3.0') = false
[deploy] checkFlowDependencies('foo@2.4.0') -> PASSED (2.4.0 does not match the denied version - correct)
[deploy] checkFlowDependencies('foo') -> REJECTED: module_not_allowed <- BUG
[runtime] importModule('foo@2.4.0') -> MODULE_NOT_FOUND (check passes, then hits #4541)
[runtime] importModule('foo') -> module_not_allowed <- BUG
== denyList ['foo@2.x'], installed foo 2.4.0, flow DECLARES foo@1.0.0 ==
[deploy] checkFlowDependencies('foo@1.0.0') -> PASSED (checked against the declared 1.0.0, not the installed 2.4.0)
== allowList ['foo@2.x'], installed foo 2.4.0, flow declares foo@1.2.0 ==
[deploy] checkFlowDependencies('foo@1.2.0') -> REJECTED: module_not_allowed
(false rejection: the module actually loaded would be the installed, allow-listed 2.4.0)
In the editor, case 1 shows up as: function node listing module foo → deploy fails with "Module not allowed"; listing foo@2.4.0 → deploy succeeds but the node starts with "Error: Cannot find module 'foo@2.4.0'" (#4541).
Expected Behavior
- Version-scoped rules should apply only to the versions they name. For an already-installed module, an unversioned reference should be evaluated against the installed version, not blanket-matched by any version-scoped rule for that name.
requireModule/importModuleshould parse the spec first (parseModuleNameis right there) and passparsedModule.moduleandparsedModule.versiontocheckModuleAllowed— currently they pass the raw spec string andnull(externalModules.js:87, :108).- Deploy-time and load-time checks should agree, so a flow that passes
checkFlowDependenciesdoes not fail at node startup and vice versa.
Environment
- Node-RED version: current
main(5.0.8) — externalModules.js:87, :108, :303, :343; util.js:134-147 - Node.js version: 26.x
- Platform/OS: any
- npm version: n/a
Related: #4541 (versioned spec fails to import) — fixing that alone would not resolve the check inconsistencies described above.
Activity