Upgrade transitive dependencies with PNPM: Fix the security vulnerabilities without breaking things

Fixing security vulnerabilities may be a frustrating task, especially when it involves transitive dependencies. Learn how to upgrade them without affecting your direct dependencies.
Gao
GaoFounder
April 24, 20244 min read
Upgrade transitive dependencies with PNPM: Fix the security vulnerabilities without breaking things

Nowadays, security vulnerabilities are a common issue in software development. Luckily, we have tools like GitHub Dependabot to help us keep our dependencies up-to-date with automated detection and pull requests.

Dependabot

However, it doesn't always work as expected. Since some dependencies are transitive, upgrading them may be a diffult task for these tools since they don't know the impact of the changes and what decision to make when there are conflicts. We need to handle these cases manually.

Methods that do not work

  • Official commands like pnpm up will mess up your pnpm-lock.yaml file. There's an issue about this which is still open at the time of writing.
  • Install the latest version of the direct dependency that has the target transitive dependency may not work. If the direct dependency didn't upgrade the version definitions, the transitive dependency will not be upgraded since it has been resolved and locked in the pnpm-lock.yaml file.

The solution

The overrides field is a powerful feature in PNPM that allows you to override some version resolutions. We'll use this feature to upgrade transitive dependencies and make the change as minimal as possible.

Let's use the above Dependabot alert as an example. It tells us that the follow-redirects package has a security vulnerability, and the patched version is 1.15.6. However, the direct dependency gatsby uses axios which depends on [email protected].

Step 1: Find the transitive dependency

There are many ways to locate the transitive dependency, but I would recommend the most straightforward way: search the pnpm-lock.yaml file.

Try to search /follow-redirects@ instead of follow-redirects to see the version resolutions.

How about "pnpm why"?

The pnpm why <package> command is useful indeed. However, it may confuse you in this case. For example, when I run pnpm why follow-redirects, here's a part of the output:

dependencies:
gatsby 5.13.4
└─┬ axios 0.28.0
  └── follow-redirects 1.15.5
gatsby-plugin-alias-imports 1.0.5
└─┬ gatsby 5.13.4 peer
  └─┬ axios 0.28.0
    └── follow-redirects 1.15.5
# ...

Actually, there's only one resolution for follow-redirects in the pnpm-lock.yaml file. The pnpm why command may show you multiple paths that depend on the same version of the package.

Step 2: Add the overrides

The eaiest case is that only one resolution exists in the pnpm-lock.yaml file. You can add the override directly to the package.json file:

{
  "overrides": {
    "follow-redirects": "^1.15.6"
  }
}

If you are in a workspace, you should add the override to the workspace's root package.json file.

Step 3: Apply the changes

Run pnpm install to apply the changes. We can see the follow-redirects package is upgraded to 1.15.6 in the pnpm-lock.yaml file with minimal changes.

Now you can remove the overrides field from the package.json file to keep it clean. Then run pnpm install again to validate the changes can be applied without the overrides field.

Troubleshooting

The above steps are the ideal case. Things may not always go as expected. Here are some tips for troubleshooting:

The version is reverted after removing the "overrides" field

This may happen when the dependent package has a fixed version or a range that doesn't include the target version. Check the package.json file of the dependent package, in this case, axios, to see if this applies.

If so, you need to keep the overrides field in the package.json file until the dependent package upgrades the version definitions.

There are multiple resolutions for the transitive dependency

As the project grows, the pnpm-lock.yaml file may float with multiple resolutions for the same package. For instance, there may be two major versions of the same package foo:

/[email protected]:
  resolution: ...
/[email protected]:
  resolution: ...

The vulnerability report shows that [email protected] has a security issue which is fixed in 1.0.1, and [email protected] is not affected. We could not simply add an override to foo:

  • If we add an override "foo": "^1.0.1", the [email protected] will be downgraded to 1.0.1. This may break the project since the [email protected] may have some new features that are used in the dependent packages.
  • If we add an override "foo": "^2.0.0", the [email protected] will be upgraded to 2.0.0. This may break the project since the [email protected] may have some breaking changes.

Assuming foo follows Semantic Versioning, we can add an override like this:

{
  "overrides": {
    "foo@<1.0.1": "^1.0.1"
  }
}

This will only upgrade the [email protected] to 1.0.1 and keep the [email protected] unchanged.

Conclusion

Before PNPM supports upgrading transitive dependencies directly, the overrides field is a good workaround to fix security vulnerabilities without breaking things. I hope this article helps you to handle these cases more efficiently. In Logto, we use this method to keep our dependencies up-to-date and secure.