Skip to main content
ANVISoftware Solutions
Lesson 12 of 14Advanced18 min

Dependency Vulnerabilities

By the end of this lesson

Find and update vulnerable packages before they are exploited.

Count the code in the orders application. A few tens of thousands of lines you wrote, and several hundred packages you did not — the web framework, the data access library, the JSON serialiser, the logging pipeline, the front-end build toolchain, and everything each of those depends on in turn.

All of it runs with your application's privileges. A weakness in a package you have never opened is a weakness in your application, and it arrives without a commit, a review or a deployment on your side: an advisory is published and the code you shipped last month is now known to be vulnerable.

That is why this is the module's first lesson rather than a footnote. Most of what a security review finds in a mature codebase is not a flaw the team wrote. It came in through the dependency tree, and the work is not clever analysis — it is knowing what you depend on, checking it regularly, and being able to update without drama.

This lesson assumes you have added a NuGet or npm package before, and can read a project file. The commands are the .NET and Node ones; the reasoning applies to any package manager.

Finding what you are carrying
Shell
# Packages with published advisories, including ones you never chose.
# Leaving off --include-transitive is the most common reason this looks clean.
dotnet list package --vulnerable --include-transitive

# Behind the latest version. A different question: outdated is not vulnerable.
dotnet list package --outdated

# Anything no longer maintained by its author, which is a slower-moving risk.
dotnet list package --deprecated

# Front end. --omit=dev narrows it to what ships, though a compromised
# build-time package still runs on your build agent.
npm audit
npm audit --omit=dev

# Reproducible restores: install exactly what the lock file records, and fail
# if the manifest disagrees with it rather than quietly resolving something new.
npm ci
dotnet restore --locked-mode
  • The --include-transitive flag is the important one. Without it you see only the packages named in your project files, which is a small fraction of what you ship, and the report comes back reassuringly empty.
  • The vulnerable listing is a comparison against advisory data, so the answer depends on when you ask. A clean result from last month means nothing today. That is the argument for running it in the pipeline on a schedule, not only when someone thinks of it.
  • Outdated and vulnerable are separate reports because they are separate decisions. A package four minor versions behind with no advisory is technical debt you can plan; one with an advisory is work with a clock on it. Conflating them produces either constant churn or a backlog nobody reads.
  • The deprecated listing is worth a monthly glance. A package whose author has stopped maintaining it will not be fixed when an advisory does arrive, so finding a replacement is better done calmly than under pressure.
  • npm audit reports severity from the advisory's own point of view, not yours. A high-severity finding in a package used only by your test runner is not the same as the same finding in your request pipeline. Read what the package does before reacting to the colour.
  • npm ci and restore --locked-mode make the build reproducible: the same inputs every time, and a loud failure when the lock file and the manifest have drifted apart. The .NET side needs RestorePackagesWithLockFile enabled in the project first, otherwise there is no lock file to enforce.
  • Add an SBOM step if you have anywhere to put it. A software bill of materials is a machine-readable list of what an artefact contains, and its value is on the day an advisory lands: you can answer which builds are affected in minutes rather than by checking out old tags.

The terms that make the rest of this lesson precise:

Direct dependency
A package named in your own project file. You chose it, you can see it, and you decide its version. This is the part of the tree people picture when they think about dependencies, and it is usually the smaller part.
Transitive dependency
A package pulled in because something else needs it. You did not choose it and may not know its name. In a typical .NET web application the transitive count is several times the direct count; in a front-end build it can be two orders of magnitude larger.
Lock file
A record of the exact versions a restore resolved, committed alongside the code. It makes builds reproducible: your machine, a colleague's and the build agent install the same tree, and a version change becomes a reviewable diff rather than something that happens on its own.
Advisory
A published report that a particular version range of a package has a known weakness, usually with an identifier and a severity. Tools compare your tree against advisory databases. An advisory is a statement about the package, not about whether your application reaches the affected code.
Version range and pin
A range accepts anything matching a pattern; a pin fixes one version. Pinning plus a lock file gives you reproducibility and puts every change under review, at the cost of updates never arriving on their own — which is the right trade only if something else brings them.

Transitive dependencies are the difficult half of this, for a simple reason: you cannot upgrade what you do not reference. The report names a package three levels down, and there is no line in your project file to change.

The first move is to upgrade the parent. If the package that pulled in the vulnerable version has a newer release that depends on a fixed version, one change to a direct dependency resolves the finding properly, and the dependency graph stays consistent.

When no such release exists, the fallback is to add a direct reference to the patched version yourself. In .NET, a direct reference wins over a transitive one, so naming the fixed version in your project file lifts it for everything. It works, and it is an override: you now have a package in your project file that your code never uses, holding a version its parent was not tested against. Comment the line with the advisory it addresses and a note to remove it once the parent catches up, because otherwise it becomes a permanent mystery.

Central package management makes both moves less painful. One file at the root of the solution declares the version for every package, and every project uses it. Instead of the same override edited in nine project files and missed in the tenth, there is one place to change and one place to read.

The honest caveat: not every finding can be fixed the week it appears. Sometimes the parent is abandoned, sometimes the fixed version needs a framework upgrade you cannot schedule yet. In that case, record the decision — the advisory, why it cannot be fixed now, whether your application reaches the affected code path, what compensating control is in place, and a date to look again. A documented accepted risk with a review date is a reasonable engineering position. An ignored report is not, and the difference is entirely in whether anyone wrote it down.

A routine that keeps this manageable. The aim is a small constant stream of updates rather than an annual event:

  1. Turn on automated update pull requests

    Tooling that opens a pull request per update, on a schedule, does the tedious part. Group patch and minor updates together weekly so the volume is reviewable, and let major versions come through individually because they need reading. The value is not the automation of the edit; it is that updates arrive as ordinary reviewed changes instead of as a project.

  2. Check for advisories in the pipeline, and fail the build

    Run the vulnerable listing on every build and on a nightly schedule, and agree in advance which severities break the build. Nightly matters because an advisory published on Tuesday affects a branch nobody touched, and nothing else will tell you.

  3. Keep the test suite good enough to trust a bump

    This is the step that decides whether the rest works. If nobody believes the tests, every update needs manual checking, updates queue up, and the queue is where the risk lives. Effort spent on tests here is effort spent on being able to patch quickly, which is the capability that matters on a bad day.

  4. Update in small, frequent batches

    A handful of packages a week produces small diffs where a break is obvious and attributable. A year of updates applied at once produces a change nobody can review, a failure nobody can locate, and a strong argument for postponing it again. The frequent version is less work in total, not more, and it also means the urgent patch lands on a codebase that is nearly current.

  5. Separate what ships from what builds, and treat both as real

    A vulnerability in a runtime package affects your users; one in a build tool affects your build agent, which usually holds deployment credentials. Different urgency, both genuine. Being able to tell them apart stops you either panicking about a linter or dismissing something that runs with access to your pipeline.

  6. Record what you accept, with a review date

    When a finding cannot be fixed now, write down the advisory, the reason, whether the affected code path is reachable from your application, and when you will look again. Put it where the pipeline reads it, so the suppression expires by itself. A suppression with no expiry becomes permanent silence.

Before adding a dependency, spend five minutes on it. None of these is decisive on its own; together they tell you what you are taking on:

Recent activity
When was the last release, and the last commit? A package with no activity for two years may be finished and stable, or abandoned. Check whether recent issues get answers. What you are really asking is: if an advisory is published against this, will anyone fix it?
Number of maintainers
One maintainer means one person's availability and one account. Both matter: they may stop, and a single compromised publishing account is how a trusted package ships something it should not. Several active maintainers with review on releases is meaningfully better.
Download volume
Weak evidence, used carefully. Wide use means problems tend to be found and reported, and it also makes a package a more attractive target. A low count is not disqualifying; it does mean you are likelier to be the one who finds the bug.
Its own dependency count
Adding one package can add forty. Look at the tree before you accept it, because everything in there becomes yours to monitor. A package with no dependencies is often worth preferring on this basis alone.
Whether you need it at all
For something a short function of your own would do, writing it is frequently the better trade: no advisories to track, no updates, no supply chain. Reach for a package when the problem is genuinely hard — cryptography, parsing, time zones, HTTP — and be more sceptical when it is small.
The exact name
Copy the name from the project's own repository or documentation, not from memory, a search result or a suggestion. Typosquatting is a real and ordinary risk: a package whose name is a plausible misspelling of a popular one, published to be installed by accident. Install scripts run at install time, before any of your code, so the mistake is not caught by review of what you wrote.

Summary

  • Most weakness in a mature application arrives through dependencies rather than through code the team wrote
  • Check with transitive dependencies included, and on a schedule, because an advisory can land without any change from you
  • Transitive findings are fixed by upgrading the parent first, and by a commented direct override only when there is none
  • Lock files make builds reproducible, and automated update pull requests are what keep a pinned tree from going stale
  • A small constant stream of updates keeps the urgent patch path short; evaluate a package, and its exact name, before adding it

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Audit a real project

Run the vulnerable listing with transitive dependencies included on a project you work on, and npm audit if it has a front end.

For each finding, write down four things: whether it is direct or transitive, what the package is actually used for, what would upgrade it, and whether your application reaches the affected code. Then decide what you would do this week and what you would schedule.

Show solution

The first thing most people notice is how much of the list is transitive. That is the normal result, and it reframes the job: the work is mostly about upgrading the parents that pulled these in, not about packages you chose.

What the package is used for decides urgency more reliably than the severity label. A parsing library in the path of every incoming request is not comparable to a formatter that runs on a developer's machine, even when the advisory rates them the same.

For 'what would upgrade it': check the parent first. A newer release of the direct dependency that depends on a fixed version is the clean fix. Only when there is none should you add a direct reference to the patched version, and then comment the line with the advisory and a note to remove it.

Reachability is useful for ordering the work and poor as a conclusion. You are asserting something about today's code, and the assertion expires the next time somebody uses another part of that library. Write it down as an accepted risk with a date, so the decision resurfaces on its own.

A defensible outcome for one week: fix everything reachable from request handling, upgrade the parents that fix a group of findings at once, record the rest with reasons and review dates, and add the check to the pipeline so this audit never has to be done by hand again. The last item is the one that changes next quarter.

Think about it

The annual upgrade window

A team schedules dependency updates once a year, in a two-week window, arguing that updates are disruptive and should be batched so the disruption happens once.

Make the case for small and frequent updates instead, and state the cost of your approach honestly.

Show solution

Start with what happens to the annual window in practice. Twelve months of changes across hundreds of packages arrive as one diff. Nobody can review it. When something breaks, the cause could be any of a hundred upgrades, so debugging is bisection across a change that was applied at once. The window overruns, the remaining half is deferred, and next year's window is eighteen months of changes.

Then the part that matters more. Between windows, the codebase drifts further from current versions, so an urgent patch is no longer a small upgrade — it is a major version jump with a framework change attached. The capability you need on a bad day is the ability to ship a patched dependency the same afternoon, and the annual model deliberately gives that up.

Small and frequent inverts all of it. A weekly batch is a reviewable diff. A break is attributable, because little changed. The tree stays close to current, so an urgent patch is a minor bump. The total work is lower, because you are never paying the cost of a large, unreviewable change.

The costs, fairly: a steady stream of pull requests needs someone to look at them, and that attention is easy to let slide. It depends on a test suite people trust — without that, frequent updates become frequent manual verification and the whole thing stalls. And you will occasionally take a bad release early, where the annual team would have skipped that version entirely. That last one is real, and a short delay on brand-new releases handles most of it.

The honest middle ground, if the team will not move all the way: patch and security updates weekly and automated, feature updates quarterly, majors scheduled deliberately. It keeps the urgent path short, which is the property worth protecting.

Saved in this browser only.