Skip to content

externalModules: version-scoped allowList/denyList entries enforced inconsistently — deny-listing one version makes a module unusable in function nodes #6011

Open

Description

@rahul05ranjan

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):

  1. One deny-listed version makes the module unusable in function nodes. With denyList: ["foo@2.3.0"] (block only a known-bad version) and foo@2.4.0 installed:

    • a module ref without version (foo) is rejected at deploy with module_not_allowed, although 2.4.0 does not match the denied 2.3.0. checkFlowDependencies passes undefined as the version, and checkAgainstList (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: importModule never 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 specifying function node 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.

  2. Deny rules are checked against the declared version, not the installed one. denyList: ["foo@2.x"], installed foo@2.4.0, flow declares foo@1.0.0 → deploy check passes, because the check evaluates the declared 1.0.0 even though the module loaded into the sandbox would be the installed 2.4.0.

  3. Symmetric false rejection with a version-scoped allowList. allowList: ["foo@2.x"], installed foo@2.4.0, flow declares foo@1.2.0 → deploy rejected with module_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/importModule should parse the spec first (parseModuleName is right there) and pass parsedModule.module and parsedModule.version to checkModuleAllowed — currently they pass the raw spec string and null (externalModules.js:87, :108).
  • Deploy-time and load-time checks should agree, so a flow that passes checkFlowDependencies does 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions