A scanner still flags an upgraded package when another copy of it is in the tree, the lockfile was not updated, or the scan ran against an old build. The fix is to find which copy is flagged and which parent pulls it in. In npm, npm ls and npm explain show both.
The usual suspects
| Cause | What you see | How to confirm |
|---|---|---|
| Nested older copy | Top level shows the new version, a deeper path shows the old one | List every path to the package |
| Lockfile not committed | Your laptop is clean, CI is not | Compare the lockfile in the scanned commit |
| Old artifact scanned | Source is fixed, the image or bundle is not | Rebuild, then scan the new artifact |
| Different branch or project | Finding belongs to a service you did not touch | Check which project the finding names |
Why a second copy can exist
npm's default install strategy is hoisted. The npm docs describe it this way: non-duplicated packages install at the top level, and duplicated ones are placed within the directory structure as necessary. So if one of your dependencies asks for an older range than the rest of the tree, npm can keep the old version nested under that parent while the top level holds the new one.
You upgraded the direct dependency, but the flagged copy belongs to someone else's range. That is why a quick look at the top of node_modules can mislead you.
Find the copy and its parent
Run these from the project root:
npm ls some-package --all
npm explain some-package
The npm ls docs say the tree it prints is the logical dependency tree, based on package dependencies, not the physical layout of node_modules, and that nested packages also show the paths to the package you name. The npm explain docs describe it as printing the chain of dependencies causing a given package to be installed. Together they tell you which parent holds the old version.
Then fix the right thing
- If a parent requests an old range, upgrade that parent first.
- If no parent update exists, check whether the ecosystem offers an override or resolution setting, and record why you used it.
- Regenerate the lockfile and commit it. The npm docs say the lockfile describes the exact tree that was generated, so later installs reproduce it.
- Rebuild the artifact, whether that is a bundle or an image, so the scan sees the new contents.
- Rescan the same project and compare the finding list.
Try it yourself
Click through it at your own pace, or open it full screen.
Look at how a fixed version is shown next to the finding, and think about which dependency path would still hold the old one.
Quick check
Is the scanner wrong if it flags the old nested copy?
Usually not. If that copy is in the tree and installed, it is a real component. Whether it is exploitable is a separate question.
Do I need to delete node_modules?
Not always, but a clean install from the committed lockfile removes doubt about leftover files.
Why is the finding gone on my laptop but present in CI?
The two probably resolve different trees. Check that the lockfile in the scanned commit matches the one you tested.
Safeguard offers dependency scanning with SBOM output and reachability analysis, and the free scan lets you try it on your own project.
Sources
Last reviewed: 19 October 2026.