You’ve seen it before: a casual glance at a project’s package.json
file reveals a slew of asterisks (*) next to dependency names. While it might seem convenient to use wildcards to automatically update dependencies, the risks far outweigh the benefits.
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"react": "*",
"react-dom": "*",
"axios": "*"
}
}
This practice can disrupt production environments, compromise security, and lead to unpredictable behavior in applications. In this post, we'll dive deeper into the risks of floating dependency ranges and explore how to mitigate them for safer npm package management.
What are Wildcards and Floating dependency ranges?#
npm uses semantic versioning (semver), for communicating what kind of changes are in a release. This helps users understand the level of effort involved in updating to the latest version by specifying major, minor, and patch versions (e.g., 1.2.3).
However, npm also offers more flexibility when specifying dependencies in your package.json
file. There are a whole host of different specifiers identified in node-semver, the semver parser for node that npm uses, but wildcards and floating dependency ranges are particularly common.
- Wildcards (*): A wildcard dependency allows any version of a package to be installed. For example, specifying "lodash": "*" in a package.json file will install the latest version of Lodash, no matter how new or different it is from prior versions. While this ensures you're always up-to-date, it also introduces risks by pulling in potentially unstable or breaking changes without your explicit review.
- Floating Dependency Ranges: Floating ranges allow for flexibility in the versions that can be installed. Instead of using a strict version, developers use operators like >, ^, or ~ to indicate compatibility within certain boundaries. For example, "lodash": "^4.0.0" means any version from 4.0.0 up to (but not including) 5.0.0 can be installed. This balances keeping dependencies up-to-date with some control over which versions are allowed, but still carries the risk of breaking changes within the allowed range.
Risks of Using Wildcard Dependencies#
Floating dependencies can pull in unverified code, introduce breaking changes, or even expose your project to malicious packages. In an ecosystem as fast-paced as npm, this flexibility can quickly turn into a vulnerability, leaving your code open to security flaws and instability.
Some of the potential issues with floating dependency ranges:
- Unexpected Breakages: A minor or patch update could introduce bugs or changes that break your application.
- Security Vulnerabilities: Automatically accepting new versions may inadvertently introduce vulnerabilities in updated dependencies.
- Reproducibility: It can be harder to replicate environments consistently if versions are not locked down.
- Unstable Dependencies: New versions of dependencies may include experimental or unstable features that haven’t been thoroughly tested, potentially causing unpredictable behavior in your application.
- Dependency Conflicts: When using floating versions, dependencies may update to versions that are incompatible with each other, leading to conflicts in your project that are difficult to diagnose.
- Silent Failures: Without specific version control, critical updates (such as bug fixes or deprecations) may silently fail, especially if an update relies on a change in another package that hasn't been updated accordingly.
- Delayed Identification of Issues: Since updates are applied automatically, you may not realize a problematic change has been introduced until after it affects your application, making it harder to trace the root cause.
Wildcard dependencies can land you in some weird situations where you get unexpected behavior during installation, resulting in disruptive conflicts and compatibility issues.
Nine months ago, someone posted a controversial hot take to reddit, advocating for using an asterisk/wildcard for each dependency in package.json in private projects. This was presumably to avoid having to update as often, but the post was subsequently deleted by its author after many developers weighed in to explain why this is a bad idea. They highlighted difficulty in debugging as another potential issue with wildcards.
Like touching a hot stove, you only have to get burned by this practice once to understand why it's a bad idea.
For one, some of your dependencies may have mutually conflicting dependencies, so periodically you will have to go in and pin some of your packages, anyway. But at least you have a chance to detect that possibility early when a build fails. Then all you have to do is find a good package-lock.json to revert to.
The other problem is when no two of your colleagues (nor build systems) is working on the same versions because actually updating packages all the time is not something that is typically done so subtle, undiscovered bugs are likely to slip through the cracks because nobody tested that specific set of dependencies. And by the time the issue is discovered, you don't know what dependency versions the working version of that feature was built on because they've been wiped out by an update.
Using wildcards for dependencies may seem like a convenient way to avoid frequent manual updates, but it can create a chaotic and unstable development environment. For example, if a package moves from version 1.5 to 2.0 and introduces changes that are not backward-compatible, your code that relies on the old API may break. If you have breaking changes with multiple packages, you will quickly have a huge mess on your hands, the kind of complex dependency issues that will bring you to your wit’s end.
Pinning Down Chaos: The Case for Strict Versioning in npm Projects#
While some developers may allow for flexibility with minor versions, most prefer to lock dependencies to specific versions and manually upgrade them to ensure compatibility and thorough testing. Here are a few best practices to follow wherever possible:
1. Use Exact Versions
- Recommendation: Specify exact versions for all dependencies.
- How: Use npm install <package>@<version> --save-exact or manually set versions in package.json.
- Why: Ensures reproducible builds and prevents unexpected changes.
2. Avoid Wildcards and Loose Versioning
- Avoid: *, >, >=, <, <=
- Why: These can lead to unpredictable updates and potential breaking changes.
3. Be Cautious with Caret (^) and Tilde (~) Ranges
- Caret (^): Allows updates to the most recent minor version.
- Tilde (~): Allows updates to the most recent patch version.
- Recommendation: Use these sparingly and understand their implications.
4. Use package-lock.json
- Why: Locks down all dependency versions, including nested dependencies.
- How: Commit package-lock.json to version control.
5. Semantic Versioning (SemVer)
- Understand and follow SemVer principles in your own packages.
- Respect SemVer when choosing version ranges for dependencies.
Socket’s Wildcard Dependency Alert#
Socket has a Wildcard Dependency alert that flags any package that has a dependency with a floating version range. It’s a medium-severity alert, because the behavior isn’t malicious but does introduce some security concerns. It flags any packages with dependencies that match the following:
- Wildcard * or empty string: Matches any version of a package, including untested or unstable versions.
- Greater than (>0 or >1): Installs any version above a certain threshold, which could pull in future versions with unknown changes or vulnerabilities.
In essence, these patterns are flagged because they allow for installing any version without strict control. Because wildcards can lead to unpredictable updates, they're often discouraged in critical systems. In these cases, you may want to update your security policy to monitor or warn.
The practice of using floating version ranges is especially common in early-stage projects or fast-evolving environments, where developers aim to stay up-to-date with the latest features or bug fixes.
However, many well-maintained projects avoid floating dependencies due to the associated risks. The popularity of floating dependencies can vary by ecosystem and the maturity of the project. It’s common to see them in open source projects, prototyping or rapid development, automation and CI/CD pipelines, and non-mission-critical projects.
Wildcard dependencies can be a ticking time bomb in your project. They might seem convenient, allowing your code to automatically adopt the latest version of a dependency, but this flexibility comes at a huge cost. By letting any version fit the criteria, you're inviting untested, unstable, or even malicious code into your project without warning. If you’re using dependencies with this alert, make sure you’re ready to monitor them and understand the risks. Otherwise, it’s a good idea to lock down your dependencies and ensure every version bump is deliberate and vetted.