NPM Lack of Version Pinning

ID

lack_version_pinning_npm

Severity

low

Family

Lack of Version Pinning

Tags

reachable

Description

Under certain circumstances (in application dependency descriptors) the versions should be pinned to avoid misbehaviour or injection of malicious dependencies (direct or indirect).

For example, under npm, a publishable package-lock.json makes dependencies pinned and avoids future injections, but existing vulnerabilities for the pinned versions should be handled.

This detector checks if the pinning file exists and is added to control version.

Security

If the project has not version pinning files as:

- package-lock.json
- yarn.lock
- npm-shrinkwrap.json

Attackers can create new public components with higher versions and the NPM installer could use the new version.

Examples

package.json
 ...
 "dependencies": {
    "nice-package": "> 1.0.0"
 }
 ...

If the attacker gains access to publish new versions of nice-package, he can create a new version "99.9.9" with malware, and NPM will happily resolve this new version with malicious code.

Mitigation / Fix

The recommendation is to pin the dependencies, typically by using exact versions in direct dependencies, and by maintaining a lock file to fix (pin) dependencies and sub-dependencies versions. For version updates, a controlled process using a dependency update tool and intelligence on potential malicious packages is more complex but much better (for security and reliability) than letting npm (or yarn) to choose versions for you.

First, you may use exact versions for direct dependencies in package.json. For example use react: "18.3.1" instead of react: "^18.3.1" (which means latest in the 18.*.* range) or react: "~18.3.1" (which means latest in the 18.3.* range). Do not let npm to choose the version. Instead, use a dependency update tool for controlled version bump.

By default, npm installs packages using ^ which pins only the major version. By installing a dependency with --save-exact, the exact version will be registered in the package.json:
# npm
npm install --save --save-exact --ignore-scripts <package>

# yarn
yarn add --exact --ignore-scripts <package>

As open source packages often use open ranges in dependencies, it is also recommended to pin transitive dependencies' versions as well: Use lock files for pinning transitive dependencies, but have your package.json to be the source of truth. Regenerate the lock file when versions are updated in version bumps (for resolving known vulnerabilities or upgrading for new features) and keep it under version control.

Of course, reviewing dependencies is always recommended. But at least an attack based on introducing malware on new versions of existing packages will not affect you immediately.

Malware packages are detected by the community or by Xygeni Malware Early Warning, and known vulnerabilities are also detected and patched regularly. You have to trade off the rate of version updates for getting security and quality patches but without opening the door to supply-chain attacks.

Version pinning helps with reproducible builds, and forces version upgrades not to be fully automatic.

The biggest drawback of pinning is its impact on library development. When publishing a library to npm with dependencies pinned, the limited range of versions is likely to result in duplicates in the node_modules folder. If another package pinned a different version, the library will end up with both and the tarball size will increase (and thus the package loading times).

Less strict version pinning could be allowed if, at least, direct dependencies are pinned and a check on potential malicious packages for the exact version of transitive dependencies is enforced.

References: