When building a Node.js project, you often rely on third-party libraries to speed up development and take advantage of community-supported solutions. For example, you might use packages like axios
to make HTTP requests or express
to set up a server. These libraries themselves often have dependencies packages they rely on to work. This chain of dependencies can become complex, and vulnerabilities in any package can potentially expose your application to security risks.
This is where npm audit comes into play.
What are audit issues?
In a typical Node.js project, when you install a package, you’re not just pulling in that specific library you’re also pulling in its entire dependency tree. For example:
- If you install
axios
, it relies on packages likefollow-redirects
andform-data
. form-data
, in turn, may depend on other packages likecombined-stream
,asynckit
, etc.
This leads to a multi-level dependency tree, which can contain hundreds or even thousands of packages. Since you may not directly work with all these sub-dependencies, it’s hard to manually check each one for vulnerabilities.
Audit issues refer to known vulnerabilities in any of these packages whether in your direct dependencies (e.g., axios
or express
) or their sub-dependencies. npm audit scans this entire dependency tree for security vulnerabilities based on a public vulnerability database maintained by npm.
What does npm audit
do?
The npm audit
command checks your project’s dependencies for security issues by comparing them to a known vulnerability database. It scans your package.json
, package-lock.json
, and node_modules
folders for any vulnerable packages.
It checks for:
- Security vulnerabilities: These could be code flaws, deprecated functions, or unsafe practices that leave your application open to security exploits.
- Severity levels: Vulnerabilities are rated by severity low, moderate, high, or critical based on the potential impact on your application.
After running npm audit
, you’ll see a detailed report with the following information:
- Vulnerable Package: The package that contains the vulnerability.
- Vulnerable Version: The version of the package that is flagged as insecure.
- Patched Version: The version that fixes the issue.
- Severity: Whether the vulnerability is low, moderate, high, or critical.
- Paths: This shows the chain of dependencies that led to the vulnerable package.
- Advisory URL: A link to the npm advisory that provides more information on the vulnerability.
Example npm audit report
# npm audit report
minimatch <3.0.5
Regular Expression Denial of Service - https://github.com/advisories/GHSA-f8q6-p94x-37v3
fix available via `npm audit fix`
node_modules/minimatch
glob 3.2.3 - 5.0.15
Depends on vulnerable versions of minimatch
node_modules/glob
express >=4.13.0
Depends on vulnerable versions of glob
node_modules/express
This report tells you that:
- The package
minimatch
, which is less than version3.0.5
, has a Regular Expression Denial of Service vulnerability. - The vulnerable
minimatch
package is used byglob
, which is a sub-dependency ofexpress
.
Severity levels in npm audit
Vulnerabilities in the npm ecosystem are categorized into different severity levels based on the potential risk they pose:
- Low: Minor vulnerabilities that are not likely to be exploited easily.
- Moderate: Vulnerabilities that might be exploited but have limited impact.
- High: Significant vulnerabilities that could cause considerable damage if exploited.
- Critical: Serious vulnerabilities that are easy to exploit and could lead to severe issues like data breaches or server takeover.
In the example above, the minimatch
package is flagged for a Regular Expression Denial of Service (ReDoS) vulnerability, which can be a serious performance issue if an attacker crafts a malicious regex pattern.
Fixing npm audit issues
The npm audit
tool doesn’t just tell you about the vulnerabilities; it also suggests how to fix them.
Running npm audit fix: For many vulnerabilities, npm audit
provides an automatic fix. You can run:
npm audit fix
This command attempts to upgrade your packages to the non-vulnerable versions while preserving backward compatibility. It will install the patch versions (those that follow semantic versioning) of your packages, meaning you won’t have to worry about breaking changes.
Using npm audit fix —force: In some cases, the automatic fix may require breaking changes, where newer versions of dependencies have major version bumps. To fix these, you can use the --force
option:
npm audit fix --force
This will attempt to install the latest version of the packages, even if that includes major version updates, which might break compatibility with your existing code. Be careful with --force
, as it can install updates that might break your code by causing compatibility issues.
In the earlier example, let’s say that to fix the minimatch
vulnerability, you might need to upgrade to a newer major version of glob
, which could introduce breaking changes for express
. In this case, npm audit fix --force
will perform the necessary updates, but you’ll need to thoroughly test your project to ensure that everything still works.
Why npm audit issues persist after fixes
You might encounter situations where, even after applying the fixes suggested by npm audit, the vulnerabilities still persist. This can be frustrating, but it often happens due to deeper, nested dependencies or unresolved issues in the package metadata. Let’s go over how to resolve these issues when npm audit fix doesn’t work.
Resolving persistent npm audit issues
- Remove
package-lock.json
: Sometimes the lock file may be the cause of lingering dependency issues. Start by deleting thepackage-lock.json
file. - Remove
node_modules
: Deleting thenode_modules
directory ensures that all dependencies are reinstalled fresh - Get the latest stable version of the vulnerable package: To make sure you’re using a secure version, check the latest stable version of the vulnerable package. You can do this using the npm command:
npm show minimatch version
- Add the overrides section: If you haven’t already, you can use npm’s overrides feature (available in npm 8 and above) to force a specific version of the vulnerable package. This ensures that npm installs a non-vulnerable version of the package across all dependencies.
In your
package.json
, add or update theoverrides
section. For example, to ensure that minimatch is using a safe version, you can add:
"overrides": {
"minimatch": "10.0.1"
}
- Install dependencies again: After updating the
overrides
section and ensuringpackage-lock.json
andnode_modules
are removed. Runnpm install
to reinstall the dependencies with the updated versions. - Handling nested dependencies: In cases where the vulnerable package is deeply nested in the dependency tree (i.e., it is a sub-dependency of another package), you may need to maintain that nested hierarchy in your
package.json
to ensure the override applies correctly. For example, ifminimatch
is a dependency of a child package likeglob
, andglob
is used byexpress
, you can add an override like this:
"overrides": {
"minimatch": {
"glob": "^7.1.7",
"another-package": "^1.0.0"
}
}
This structure ensures that the override applies to all instances of minimatch
within your dependency tree, even if they are nested.
By following these steps, you can resolve persistent npm audit
issues that remain after running npm audit fix. This method ensures that even deeply nested vulnerabilities are addressed by forcing secure versions of packages across the entire dependency tree.
Preventative measures: Best practices for managing dependencies
-
Regularly run npm audit: Make it a habit to run
npm audit
regularly, especially when adding new dependencies or before pushing your code to production. This helps catch vulnerabilities early and prevents them from accumulating. -
Keep dependencies up-to-date: Use tools like
npm outdated
ornpm-check
to see if any of your dependencies have newer, more secure versions available. Keeping your dependencies up-to-date is a crucial step in reducing your exposure to vulnerabilities. -
Use
npm audit --production
: If some vulnerabilities are only relevant to development dependencies (like build tools or testing frameworks), you can narrow the audit to just production dependencies by running:
npm audit --production
This ensures you only see issues that might affect your deployed application.
- Switch to actively maintained libraries: If you’re using a library that is no longer actively maintained and has unresolved security issues, consider switching to a more actively maintained alternative. For example, if
jsonwebtoken
was no longer maintained and had critical vulnerabilities, you might switch to another popular library for handling JWTs, such asnode-jose
.
Conclusion
npm audit
is an invaluable tool for identifying and fixing security vulnerabilities in your Node.js projects. It helps you navigate the complex web of direct and transitive dependencies by highlighting known security issues and guiding you toward solutions.
However, automatic fixes might not always be possible without introducing breaking changes, and sometimes vulnerabilities may persist even after updates. Understanding how npm dependencies work, staying up-to-date with package versions, and knowing when to apply manual patches can go a long way in ensuring the security of your Node.js applications.
By adopting a proactive approach to dependency management through regular audits, automated fixes, and careful patching you can keep your application secure and up-to-date, minimizing the risk of security breaches.