Is Your Lockfile Hiding Vulnerabilities?

A lockfile is the most honest document in your repository: it lists the exact version of every package your build actually installs, no ranges, no “latest”, no surprises. It is also, quietly, a list of attack surface — every pinned version is a version somebody might have found a hole in since you pinned it. This article explains how a scanner turns that list into answers: how the OSV database matches versions to known vulnerabilities, why a CISA KEV flag matters more than a CVSS score, and why “no vulnerabilities found” is a statement about a database, not about your code. You can run the check yourself in the SBOM Vulnerability Scanner, which parses your lockfile in the browser and never uploads it.

What a lockfile pins — and why it is the attack surface

Modern software is mostly other people's code. A typical web application pulls in hundreds of packages, and each of those pulls in more: a direct dependency on a web framework can easily become two hundred transitive packages you never chose and never reviewed. The lockfile is the record of that tree — package-lock.json for npm, a fully pinned requirements.txt for Python, go.mod and go.sum for Go — written down so that every install, on every machine, produces the same tree. Without it, “it works on my machine” is a hope, not a fact.

That same exactness is what makes the lockfile the right place to look for known vulnerabilities. A vulnerability database is keyed by package name, ecosystem and exact version: lodash 4.17.20 is vulnerable, lodash 4.17.21 is not, and the difference is one lookup. A floating range cannot be judged that way — the scanner would have to guess which version you actually installed, and guessing is not a security practice. So the first rule of lockfile hygiene is unglamorous and non-negotiable: commit the lockfile, keep it pinned, and scan the pinned tree, not the wish list in your manifest.

How OSV matches versions to known vulnerabilities

OSV.dev is a public vulnerability database that aggregates advisories from GitHub, the NVD and the language ecosystems into one queryable index. A scanner sends it a batch of name, ecosystem and version triples — lodash, npm, 4.17.20 — and gets back the vulnerability ids that affect exactly that version. The ids are then fetched in full: a summary, the affected ranges with their fixed versions, the severity as a CVSS vector, and the CVE aliases. Nothing about your project travels with the query — just the public coordinates of public packages.

The match is deliberately dumb, and that is its strength. It does not read your code, does not judge whether you call the vulnerable function, and does not care how the package is used. If the advisory says versions before 4.17.21 are affected and you pin 4.17.20, you match — even if your code never touches the vulnerable function. That over-reporting is a feature: reachability analysis is a harder, slower question, and the cheap question (“is this version known-bad?”) already catches the dependency you forgot to upgrade three years ago.

What the scanner cannot see

A version-matching check has a hard boundary, and it is worth stating plainly: it can only find vulnerabilities somebody has already reported, for a version the database knows about. A vulnerability discovered yesterday and not yet published is invisible. A package that was never vulnerable but is malicious — a typosquat of a popular name, a compromised release, a dependency that phones home — has no CVE at all, and no scanner of this kind will flag it. The same goes for your own code: the scanner judges the packages you install, not the code you write.

The boundary is why the result “no vulnerabilities found” needs careful reading. It means: for these exact versions, in this database, at this moment, nothing is recorded. It is a useful negative — a clean scan of a fresh lockfile is genuinely good news — but it is not a certificate. The defences for what the scanner cannot see are different tools: pinning and reviewing new dependencies before they enter the tree, watching for new advisories continuously rather than quarterly, and treating a sudden new release of a quiet package with suspicion. The scanner is the smoke alarm, not the fire brigade.

Why a KEV flag outranks a high CVSS score

A CVSS score answers a hypothetical: how bad would this vulnerability be if someone exploited it? The CISA Known Exploited Vulnerabilities catalog answers a fact: is someone exploiting it, in the wild, right now? The two answers routinely disagree. A vulnerability scored 9.8 that nobody has weaponised can wait for the next planned upgrade; a vulnerability scored 5.3 that is on the KEV list — with a remediation due date set by CISA — deserves a patch this week, because the exploit already exists and is already in use.

That inversion is why the scanner badges KEV rows separately instead of folding them into the severity sort. Sort order tells you what is bad; the KEV badge tells you what is urgent. In practice the triage is short: patch the KEV items first, then the criticals with a published fixed version, then schedule the rest with the next upgrade cycle. The Bulk CVE Triage desk on this site puts the same CVEs in exploitation context — KEV status, EPSS probability and CVSS together — when a single badge is not enough to justify the work to a team.

Commit the lockfile, scan it in CI — not once a quarter

A lockfile that is not committed is a lockfile that does not exist. If it lives only on one developer's machine, every other machine installs whatever “latest” resolves to that day, and the tree you scanned is not the tree you ship. Committing it is the cheapest supply-chain control there is: it makes installs reproducible, makes the scanner's answers true for everyone, and turns a dependency upgrade into a reviewable diff instead of a silent drift.

The second half is cadence. Vulnerabilities are published every day, so a scan run once a quarter is a snapshot that ages in days. The useful shape is a scan on every pull request that touches the lockfile, plus a scheduled scan of the main branch — the same loop as a test suite, with the same property: a new advisory turns a green build red the day it is published, when the fix is a version bump, not a post-incident review. The browser scanner on this site is the manual version of that loop: paste the lockfile after a dependabot bump, or before a release, and confirm the tree you are about to ship.

Fixed versions, breaking changes, and when to patch vs pin

Every match in a scan carries a fixed version — the first release in which the vulnerability is gone — or an honest “none published”. The common case is a patch or minor release, and the upgrade is a one-line change in the lockfile. But a fixed version is not always a safe version to take: a major-version jump can change behaviour your code depends on, and a fix released yesterday has had a day of real-world testing. The judgement call is the same as any dependency upgrade: read the changelog, run the tests, and upgrade the packages you actually import first.

When there is no fixed version, the options narrow to three: remove the dependency if nothing needs it, replace it with a maintained alternative, or mitigate the vulnerable code path — stop passing untrusted input to the affected function, or put the call behind a check the advisory describes — and watch the advisory for an update. What you should not do is pin the vulnerable version and forget it: a pin is a decision, and it deserves a comment in the lockfile or the issue tracker saying why, and a date to revisit it. Re-scan after any change; the scanner takes seconds, and a second scan is the only proof the first scan's findings are gone.

Sources

Frequently asked questions

My scan found nothing. Is my project safe?

It is good news, not a guarantee. A clean scan means no vulnerability has been reported for your exact pinned versions in the OSV database. It says nothing about unreported vulnerabilities, malicious packages, or your own code. Keep the lockfile committed, re-scan when it changes, and treat the scan as one layer of defence — the SBOM Vulnerability Scanner explains its own boundary on the page.

Why does the scanner need exact versions?

Because vulnerability databases are keyed by exact version. A range like >=1.2 or ~=2.0 does not identify a single version, so the scanner cannot say whether the version you actually installed is affected. Pin exact versions in the lockfile — that is what lockfiles are for — and the scanner can judge every entry in one lookup.

A fix would force a major-version upgrade. Should I still patch?

For a KEV-listed vulnerability, yes — patch first and deal with the breakage, because the exploit is already in use. For everything else, weigh the severity against the breakage: upgrade the packages your code actually imports, run the tests, and if the major jump is too risky, mitigate the vulnerable code path and schedule the upgrade. Re-scan after the change to confirm the finding is gone.