Transitive dependency fixes for npm: how Dependabot got there
Dependabot's security updates have long handled the straightforward case: an advisory affects a direct dependency, and a patched version exists within the declared version range. The harder case is when the vulnerable package sits several levels down the dependency tree, behind a chain of intermediate packages whose own version requirements block the fix. Those jobs used to end with an "update not possible" error, leaving the developer to manually walk the ancestor chain and figure out which versions to bump.
For npm projects, that error is now far less common. Dependabot can resolve the full chain of dependencies needed to lift a vulnerable transitive package to a safe version, then open a pull request that explains the path from the vulnerable package up to a direct dependency.
Why the chain blocked updates
Dependabot security updates aim for the most conservative fix that removes the vulnerability while respecting existing version requirements. With version updates, by contrast, Dependabot can be configured to widen or increase a requirement to accommodate the latest release. The distinction matters when a vulnerable package is reachable only through a sequence of intermediate dependencies, each with narrow version constraints.
Consider an npm project that depends on react-scripts@^4.0.3. The caret range permits any version from 4.0.3 up to, but not including, 5.0.0. In March 2022, a high-severity advisory was published for node-forge, a cryptography and networking toolkit, affecting versions before 1.3.0. The project has no direct dependency on node-forge, but the dependency tree pulls in version 0.10.0 through this chain:
[email protected]permits only[email protected][email protected]permits only[email protected][email protected]permits only[email protected]
To get node-forge to the patched 1.3.0, Dependabot would need to find a version of selfsigned that allows [email protected], then a version of webpack-dev-server that allows that selfsigned, and so on up to react-scripts. That resolution logic simply wasn't in place, so the security update failed.
Why npm was the first target
Data from Dependabot's own usage pointed clearly at JavaScript. More than 80% of security updates Dependabot performs are for npm and Yarn projects, so improving outcomes there would have the largest effect. Both npm and Yarn can audit a project's dependency tree for known vulnerabilities, but only npm natively goes the extra step and performs the updates needed to resolve what it finds. After an engineering spike confirmed the approach was feasible, Dependabot's team moved to productionize it.
Borrowing Arborist's audit machinery
The npm audit command collects the project's dependencies, sends a bulk request to the npm registry for all affecting advisories, and produces a report. That report lists each vulnerable package, the dependency that requires it, the relevant advisories, and whether a fix exists — nearly everything needed to unlock a transitive dependency. Two gaps remained:
- The report doesn't trace the chain from a vulnerable transitive package back to a direct dependency. Since the final pull request should explain the change in terms a developer recognizes, that context had to be reconstructed.
- Dependabot processes one vulnerable dependency per security update to keep diffs small and avoid breaking changes.
npm auditandnpm audit fix, however, operate across the whole project, making it impossible to tell which resulting updates mattered for the dependency in question.
The underlying audit functionality is exposed through Arborist, the npm component responsible for managing dependency trees. Dependabot is a Ruby application, so the team wrote a helper script that invokes Arborist.audit() in a subprocess. The script takes a vulnerable dependency and the list of advisories affecting it, and returns the updates npm recommends.
For the first gap, the script uses the audit results to perform a depth-first traversal of the dependency tree starting at direct dependencies. That keeps the full ancestor chain intact — useful information for the pull request — and runs in time linear in the total number of dependencies.
For the second gap, the script exploits the Arborist constructor's option for a custom audit registry URL. A mock registry server built with nock returns advisory data only for the dependency passed into the script, scoping the audit to that single package. The team sees both capabilities — linking a vulnerable package to its top-level ancestor and auditing for a specific package or vulnerability set — as potential additions to Arborist and is working to contribute them upstream.
On the Ruby side, the output from the helper script is parsed and verified, with handling for cases where fixing a vulnerability means downgrading or removing a dependency. Once a viable update path is identified, the rest of the security update job proceeds normally and a pull request is opened that identifies both the transitive dependency and its top-level ancestor.
A caveat on force updates
When npm audit determines that a vulnerability can only be fixed by crossing a major version boundary, it requires the force option on npm audit fix. With that option, npm updates to the latest version of the package, even across multiple major versions. This departs from Dependabot's prior security update behavior, but it's what makes unlocking the conflicting chain possible. As always, changelogs deserve a review before jumping major versions.
Measured impact
Support for transitive security updates in npm projects rolled out in September 2022. Comparing the first and fourth quarters of that year, update-not-possible errors for JavaScript security updates dropped by 42%. Projects with Dependabot security updates enabled needed no configuration changes to benefit.
What's next
The work highlights a broader design principle: leverage the package manager's native capabilities whenever they exist. Those capabilities differ widely across ecosystems, and Dependabot's integration strategy has to adapt accordingly. The hope is that other package managers eventually introduce audit-and-fix functionality comparable to npm's, at which point the same transitive update support can be extended to their ecosystems.



