Niraj Chauhan

Niraj Chauhan

#father #husband #SoftwareCraftsman #foodie #gamer #OnePiece #naruto

Understanding npm audit and fixing vulnerabilities

Posted by on

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.

Architect drawing

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 like follow-redirects and form-data.
  • form-data, in turn, may depend on other packages like combined-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 version 3.0.5, has a Regular Expression Denial of Service vulnerability.
  • The vulnerable minimatch package is used by glob, which is a sub-dependency of express.

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.

npm-meme

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

  1. Remove package-lock.json: Sometimes the lock file may be the cause of lingering dependency issues. Start by deleting the package-lock.json file.
  2. Remove node_modules: Deleting the node_modules directory ensures that all dependencies are reinstalled fresh
  3. 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
  1. 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 the overrides section. For example, to ensure that minimatch is using a safe version, you can add:
"overrides": {
  "minimatch": "10.0.1"
}
  1. Install dependencies again: After updating the overrides section and ensuring package-lock.json and node_modules are removed. Run npm install to reinstall the dependencies with the updated versions.
  2. 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, if minimatch is a dependency of a child package like glob, and glob is used by express, 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

  1. 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.

  2. Keep dependencies up-to-date: Use tools like npm outdated or npm-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.

  3. 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.

  1. 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 as node-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.