Mission

The protobufjs RCE That Rode in Through firebase-admin

The protobufjs RCE That Rode in Through firebase-admin

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.x can 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.

Frequently Asked Questions

What is the protobufjs RCE vulnerability?

GHSA-xq3m-2v4x-88gg is a prototype-pollution flaw in protobufjs that an attacker can chain into remote code execution, rated CVSS 9.8 (critical). It affects vulnerable versions of the protobufjs package, which is widely pulled in as a transitive dependency by Google Cloud and Firebase SDKs.

How did firebase-admin pull in a vulnerable protobufjs version?

firebase-admin depends on @google-cloud/firestore, which historically pinned an older protobufjs range. Even after Google patched the direct dependency upstream, lockfiles in long-lived projects kept the vulnerable transitive version installed until something forced a refresh.

How do I check if my project is affected?

Run `npm audit` for a top-level signal, then `npm ls protobufjs` to see every path that pulls it in and at what version. Anything below the patched range is a candidate for the fix below.

How do I fix a vulnerable transitive dependency without waiting for the upstream package?

Use an `overrides` block in your root package.json (npm 8.3+) to force the patched version everywhere in the dependency tree, then delete node_modules and the lockfile and reinstall. This is the standard pattern when the maintainer hasn't released a patched parent yet.

Why should MSPs care about transitive CVEs specifically?

Transitive CVEs don't show up in your direct dependency list, so they survive code reviews, are missed by spot checks, and only surface when something scans the full dependency tree. For MSPs running customer code in production, an unpatched transitive RCE is the same liability as a direct one — the attacker doesn't care which line of package.json got them in.