This morning we cleared a CVSS 9.8 RCE out of a production Node.js application. The vulnerable package wasn't anywhere in our package.json — it was three levels deep, pulled in by code we trust every day. If you run anything on top of firebase-admin, the Google Cloud SDK, or really any Google-flavored Node service, this one is worth ten minutes of your morning.
Here's exactly how it surfaced, how we fixed it, and what it should change about how you (or your MSP) think about dependency hygiene.
What's Actually Wrong With protobufjs
The advisory is GHSA-xq3m-2v4x-88gg — a prototype-pollution bug in protobufjs that an attacker can chain into remote code execution. CVSS 9.8. Network-reachable, no privileges required, no user interaction. The protobufjs package decodes Protocol Buffer messages — the binary serialization format Google uses for almost everything — so any service that takes a Protobuf payload from a less-trusted source is in scope.
The patch landed in protobufjs upstream weeks ago. The problem isn't the patch. The problem is who's still installing the old version, and why.
The "I Don't Use protobufjs" Problem
Our package.json had no direct dependency on protobufjs. Neither, probably, does yours. Here's the chain that put it in our production container anyway:
your-app
└── firebase-admin (13.10.x)
└── @google-cloud/firestore
└── protobufjs (vulnerable range)
firebase-admin is the canonical server-side Firebase SDK. Tens of thousands of Node services depend on it directly. It pulls in @google-cloud/firestore, which historically pinned a protobufjs range that includes the vulnerable versions. Even after Google fixed the upstream dependency, projects with a lockfile from a few months ago kept the bad version pinned — because that's exactly what lockfiles are designed to do.
So you can be on a fully-patched firebase-admin, on the latest Node LTS, with npm audit showing nothing scary at the top, and still ship CVSS 9.8 to production.
How We Found It
The diagnostic took two commands:
npm audit
npm ls protobufjs
npm audit flagged a "critical" with the GHSA ID. npm ls protobufjs then traced every path in the dependency graph that resolved to a vulnerable version — and the answer was "all of them, via firebase-admin." That's the moment you know it's transitive: there's no version of npm update protobufjs that fixes it, because protobufjs isn't your dependency to update.
If you're running this check on a customer environment for the first time, brace yourself. It's common to find several criticals living three or four levels deep that nobody has ever looked at.
The Fix: npm Overrides
When the vulnerable package is transitive and you can't wait for every parent in the chain to cut a new release, the right tool is the overrides block in your root package.json. As of npm 8.3, this lets you force a specific version everywhere in the tree:
{
"dependencies": {
"firebase-admin": "^13.10.0"
},
"overrides": {
"protobufjs": "^7.5.4"
}
}
Then:
rm -rf node_modules package-lock.json
npm install
npm ls protobufjs
npm audit
npm ls should now show every path resolving to the patched version. npm audit should drop the critical. Done — in about three minutes of actual work.
A few practical notes from doing this in anger this morning:
- Pick a version range that's compatible with the consumers. If a parent package needs protobufjs
^6.x, forcing^7.xcan break it. Read the parent's peer-dep notes before you override. - Commit the lockfile change. Overrides only travel if the lockfile travels.
- Re-run your test suite. Forcing a major version bump on a transitive dep is exactly the kind of change that surfaces edge cases in serialization. We caught zero regressions, but we ran the suite anyway.
- Drop a comment in package.json explaining why the override exists, with the GHSA link. Future you, six months from now, has no memory of this morning.
For Python shops, the equivalent pattern is pinning the vulnerable transitive directly in requirements.txt (or using pip-tools constraints), since pip has no first-class overrides system. Same idea, different mechanics.
What This Should Change for MSPs
Direct dependencies get attention. Transitive dependencies get ignored — and that's exactly why attackers like them. A few takeaways worth standardizing across every Node project you manage:
Run npm audit on every customer codebase, on a schedule. Not just at deploy time. Lockfiles freeze your dependency graph until someone forces a refresh, and an advisory published Tuesday doesn't magically remove the vulnerable code from production on Wednesday. A weekly cron that runs npm audit --production --audit-level=high against every active project and pings you on findings is a one-evening build.
Trace every critical with npm ls <package>. "It's a transitive, not our problem" is exactly the rationalization that lets criticals sit in production for months. If your dependency tree contains it, it's running in your customer's environment, and you own it.
Standardize the override pattern. Document it in your runbook. The first time a CVSS 9.8 lands on a Sunday, you don't want someone Googling "npm transitive dependency fix" while the customer is on the phone.
Audit lockfile freshness. A package-lock.json that hasn't been regenerated in a year is a snapshot of last year's vulnerability surface. Refresh lockfiles deliberately, with tests, on a cadence — even if no direct dependency has changed.
If you'd rather have a tool do the dependency-tree scanning for you across every customer environment instead of stitching together npm commands by hand, that's the lane our Radar scanner lives in. It walks the full dependency graph, flags transitive criticals, and gives you the same kind of report we built ourselves to clear this one out.
The Pattern, Not Just the Patch
protobufjs will get patched everywhere eventually. The next transitive critical is already in your tree — you just don't know its name yet. The habit worth building isn't "remember this CVE." It's checking the depth of your own dependency graph before someone else does it for you.
Catch the Critical Before It Catches You
Most MSPs find out about transitive CVEs the same way: a customer's environment trips an alert, or worse, doesn't. Oscar Six Security's Radar scans the full dependency surface — direct and transitive — and surfaces criticals before they ship to production. Through the end of May, use code BOGO2-MAY at checkout to get two scans for the price of one.
See what Radar surfaces in your stack →
Focus Forward. We've Got Your Six.