July 28, 2026
irect and Transitive Dependencies: Why Every Version Must Be Analyzed
Finding a vulnerable package is only the beginning of the investigation

By Juliano Pereira de Souza
7 min read
Finding a vulnerable package is only the beginning of the investigation
When a security tool identifies a vulnerability in a dependency, the most common reaction is to search for the package in package.json, update its version, and consider the problem solved.
But it is not always that simple.
While contributing to the documentation of OWASP cve-lite-cli, I analyzed a scenario that often creates confusion, even among experienced development teams: the simultaneous presence of direct and transitive versions of the same package.
One version may already be updated and secure, while another version, introduced through an intermediate dependency, remains vulnerable inside the application.
This means that finding the package name is only the beginning of the investigation.
In modern applications, the same library may appear multiple times in the dependency tree. One version may have been installed directly by the project, while another arrived transitively through a library used by the application.
The package name is the same. The installation path is different. The version may also be different.
That is why every occurrence must be analyzed individually.
Direct and transitive dependencies
A direct dependency is explicitly declared by the project.
{
"dependencies": {
"package-a": "4.2.0"
}
}{
"dependencies": {
"package-a": "4.2.0"
}
}In this example, the application directly depends on package-a.
A transitive dependency, on the other hand, is installed because another library requires it.
application
└── framework-b
└── package-a@2.8.0application
└── framework-b
└── package-a@2.8.0The application did not explicitly declare package-a@2.8.0, but that version is part of the dependency tree because framework-b requires it.
Now imagine that the project has the following structure:
application
├── package-a@4.2.0
└── framework-b@7.1.0
└── package-a@2.8.0application
├── package-a@4.2.0
└── framework-b@7.1.0
└── package-a@2.8.0There are two versions of the same package:
package-a@4.2.0, installed directly;package-a@2.8.0, installed transitively.
If a vulnerability affects only versions earlier than 3.0.0, the direct dependency is safe, but the transitive version remains vulnerable.
Updating only the first version does not solve the problem.
The mistake of looking only at package.json
The package.json file shows the dependencies explicitly declared by the project, but it does not represent the entire installed dependency structure by itself.
The complete tree is reflected in lock files such as:
package-lock.json;yarn.lock;pnpm-lock.yaml.
These files may contain multiple versions of the same library, different installation paths, and dependencies that were never directly selected by the development team.
When investigating a vulnerability, searching only for the package name in package.json may lead to an incorrect conclusion:
"We are already using a secure version."
That statement may be true for the direct dependency while remaining false for the application as a whole.
The correct question is not only:
Which version did we declare?
The better question is:
Which versions are present, how did they enter the dependency tree, and which of them are actually used?
The vulnerability belongs to the version, not only to the package
A security advisory normally identifies a combination of:
- package name;
- affected version range;
- vulnerability type;
- conditions required for exploitation.
Consider the following scenario:
package-a@2.8.0 → vulnerable
package-a@3.5.0 → vulnerable under a specific configuration
package-a@4.2.0 → fixedpackage-a@2.8.0 → vulnerable
package-a@3.5.0 → vulnerable under a specific configuration
package-a@4.2.0 → fixedTreating all these occurrences as equivalent creates two possible problems.
The first is a false negative: considering the project secure because one version was updated.
The second is a false positive: classifying every version as vulnerable without checking the affected range and exploitation conditions.
The analysis must happen at the version and dependency-path level.
The path reveals what introduced the dependency
Knowing that a vulnerable version is installed is not enough. It is also necessary to identify what introduced it into the project.
Tools in the Node.js ecosystem can help with this investigation:
npm ls package-a
npm explain package-anpm ls package-a
npm explain package-aFor projects using Yarn or pnpm:
yarn why package-a
pnpm why package-ayarn why package-a
pnpm why package-aThese commands may reveal a structure similar to this:
application
└── reporting-library@5.0.0
└── parser-library@3.2.0
└── package-a@2.8.0application
└── reporting-library@5.0.0
└── parser-library@3.2.0
└── package-a@2.8.0Now the team knows that the vulnerable version was not installed directly. It arrived through reporting-library, which depends on parser-library, which ultimately depends on package-a.
This path completely changes the remediation strategy.
The team may need to:
- update
reporting-library; - update
parser-library; - replace an unmaintained library;
- temporarily use an override or resolution;
- wait for a compatible update from the maintainer.
Without understanding the path, remediation becomes trial and error.
A practical case from OWASP cve-lite-cli
This problem became particularly clear during a contribution to OWASP cve-lite-cli, a tool designed to make information about known vulnerabilities easier to query and use.
The documentation needed to represent a scenario in which the same library appeared simultaneously as a direct and transitive dependency, but with different versions.
The conceptual structure was similar to this:
application
├── package-a@4.2.0
└── framework-b@7.1.0
└── package-a@2.8.0application
├── package-a@4.2.0
└── framework-b@7.1.0
└── package-a@2.8.0A superficial reading could consider only the direct version:
package-a@4.2.0package-a@4.2.0Because that version was fixed, the initial conclusion could be that the project was no longer vulnerable.
However, the dependency tree still contained:
framework-b
└── package-a@2.8.0framework-b
└── package-a@2.8.0The vulnerable version remained installed through another path.
The most important part of the documentation was not simply showing both versions. It was presenting the complete and actionable dependency path.
Compare:
package-a@2.8.0package-a@2.8.0with:
application
└── framework-b@7.1.0
└── package-a@2.8.0application
└── framework-b@7.1.0
└── package-a@2.8.0The first output identifies the problem.
The second helps the team fix it.
By seeing the full path, developers can understand that changing the direct dependency will not necessarily remove the vulnerable version. The remediation may require updating framework-b, replacing it, or carefully controlling dependency resolution.
This case reinforces an important rule:
A vulnerability should not be associated only with a package name. It should be associated with a package, version, and dependency path.
Not every installed dependency represents the same risk
The presence of a vulnerable version deserves investigation, but it does not automatically mean that practical exploitation is possible.
The analysis should also answer several contextual questions.
Is the dependency part of the production application?
A library used only for testing, building, or development may have a different exposure surface from a library executed on a server or shipped to the browser.
This does not mean development dependencies should be ignored. It means their context should be evaluated correctly.
Is the vulnerable feature actually used?
A library may provide dozens of functions while the application uses only a small portion of them.
When possible, the team should investigate whether the vulnerable code path is reachable from the application.
Can the input be controlled externally?
Many vulnerabilities require malicious data supplied through user input, external files, network requests, or other untrusted sources.
If the vulnerable code receives externally controlled data, the risk is generally higher.
Is a fix available?
Some alerts can be resolved through a simple version update. Others require replacing a major dependency or changing part of the architecture.
The remediation effort depends directly on where the package appears in the dependency tree.
A practical investigation process
A consistent dependency analysis can follow six steps.
1. Identify every installed version
Do not consider only the directly declared version.
List every occurrence of the library in the dependency tree.
npm ls package-a --allnpm ls package-a --allThe objective is to answer:
- How many versions are installed?
- Which versions are they?
- Is any of them within the vulnerable range?
2. Map the path of each version
For every occurrence, identify the complete chain:
application → dependency → subdependency → vulnerable packageapplication → dependency → subdependency → vulnerable packageThis map reveals which library needs to be updated or replaced.
3. Verify the affected version range
Compare every installed version with the security advisory.
It is not enough to know that the package has a known vulnerability. The team must confirm whether that specific version is affected.
4. Evaluate context and reachability
Check:
- where the library is executed;
- whether it is included in the production artifact;
- which functions are used;
- whether the vulnerable code path is reachable;
- whether it processes external or untrusted data.
5. Select the appropriate remediation
Possible strategies include:
- updating the direct dependency;
- updating the library that introduces the transitive dependency;
- replacing an abandoned package;
- applying a temporary mitigation;
- using
overridesorresolutionswith careful testing.
Forcing a different version may break compatibility assumptions made by the intermediate library. Overrides should therefore never be applied without validation.
6. Confirm the result
After remediation:
- reinstall the dependencies;
- inspect the tree again;
- run automated tests;
- generate a new security report;
- confirm that the vulnerable version has actually disappeared.
Changing package.json without validating the lock file and the final dependency tree is not enough.
The same library may require different decisions
Consider this scenario:
application
├── package-a@4.2.0
├── framework-b
│ └── package-a@2.8.0
└── test-runner
└── package-a@3.5.0application
├── package-a@4.2.0
├── framework-b
│ └── package-a@2.8.0
└── test-runner
└── package-a@3.5.0The analysis could reach the following conclusions:
package-a@4.2.0is fixed;package-a@2.8.0is vulnerable and used in production;package-a@3.5.0is vulnerable only under a condition that does not occur during testing.
Even though the package name is the same, each occurrence has:
- a different version;
- a different dependency path;
- a different execution context;
- a different remediation strategy;
- a different risk level.
Grouping everything into a single alert may simplify the interface, but it cannot replace technical analysis.
SBOMs and scanners are starting points
Software composition analysis tools, dependency scanners, and SBOMs are essential for providing visibility.
Projects such as OWASP cve-lite-cli also help make vulnerability information more accessible and usable within technical and automated workflows.
These tools can help identify:
- installed packages;
- installed versions;
- known vulnerabilities;
- direct and transitive dependencies;
- the paths through which packages were introduced;
- potentially fixed versions.
However, a tool does not automatically understand the complete context of an application.
It may report that a vulnerable version exists. The team must still determine:
- how the version entered the system;
- whether it is actually used;
- whether the vulnerable code path is reachable;
- what the real impact is;
- which update solves the problem without introducing regressions.
Automation finds the signal. A clear dependency tree makes the signal actionable. Technical investigation turns that signal into a decision.
Conclusion
Dependency security cannot be reduced to searching for a package name in package.json or automatically running the first update command suggested by a tool.
An application may contain multiple versions of the same library. Some may already be fixed, while others remain vulnerable through less visible transitive paths.
As demonstrated by the scenario documented during the contribution to OWASP cve-lite-cli, displaying only the package and version is not always enough. The complete dependency path may be the information that allows a team to move from a generic alert to a concrete remediation.
A complete analysis must consider:
- every installed version;
- the affected version range;
- the path that introduced it;
- the context in which it is executed;
- its reachability;
- the safest remediation strategy.
The question should not be only:
Does this package have a vulnerability?
The more useful question is:
Which version is vulnerable, through which path did it enter the application, and what must be updated to remove it?
That change in perspective is what transforms a vulnerability list into a security analysis that is genuinely useful.