Bomfather Has Been Funded by Balaji Srinivasan

GitHub’s ubuntu-latest Runners Have 1,681 Packages and 9 HIGH-Severity Vulnerabilities

GitHub runners are packed with vulnerabilities. Should we really be using them?

← Back to Blog

We build our platform in Go and C. Our production containers are stripped down to exactly what we need. Removing unnecessary packages and minimizing the attack surface. We vendor our packages.

Then we run our builds on GitHub’s ubuntu-latest runners which have 1,681 packages.

That’s absurd. We won’t run production with all this cruft, so why would we build with it? Supply chain attacks targeting build environments are increasingly common and devastating, compromising one dependency can silently infect every artifact we produce. Our build environment has access to our source code, secrets, and signing keys. If anything, it should be more locked down than production, not less.

Last week we inventoried what actually ships on ubuntu-latest. The results convinced us to move away from GitHub runners entirely.

The numbers

We ran an inventory script on a fresh ubuntu-latest runner, and the total package count is 1,681.

Breaking it down, 214 APT system packages, 162 Python packages, 173 Ruby gems, 119 Conda packages, and 13 NPM packages.

We build Go and C code. We need the Go and C compiler, git, and maybe 15 other tools. About 20 packages total.

The runner gives us 1,681 packages. That includes entire language ecosystems we never touch (Python, Ruby, Node, Conda). All sitting there, increasing our attack surface.

The vulnerabilities

We scanned 348 of those packages using OSV.dev. We could only scan the Ruby, Python, and NPM packages because APT and Conda aren’t supported by the scanner yet.

Of the 348 packages scanned, 16 packages have known vulnerabilities, with a total of 63 vulnerabilities across those 16 packages. 9 vulnerabilities were rated high, 38 moderate, 6 low, and 10 unknown.

The Python cryptography package version 41.0.7 has 6 high severity vulnerabilities.

The setuptools package version 68.1.2 has 3 high severity vulnerabilities. CVE-2024-6345 is a path traversal bug where an attacker can write arbitrary files anywhere on the filesystem. That means someone can inject malicious code directly into your build artifacts.

We don’t use Python, and we definitely don’t import cryptography or setuptools. But they’re there in our build environment.

Would we run production with known high severity vulnerabilities? No.

Would we ship a container with cryptography 41.0.7 knowing it has 6 high CVEs? Never.

So why is it okay in our build environment?

The full list

For transparency, here are all 16 vulnerable packages we found:

Jinja2 has 10 vulnerabilities. The cryptography package has 10 with 6 being high. Twisted has 6. Setuptools has 6 with 3 being high. Then certifi, idna, requests, urllib3, and python-apt each have 4. Configobj, cgi, webrick, and net-imap each have 2. Finally rdoc, resolv, and uri each have 1.

These are just the packages we could scan. We couldn’t scan the 1,214 APT packages or the 119 Conda packages. The actual vulnerability count is almost certainly higher.

The code for getting the vulnerabilities is here, bomfather/tools.

Attack surface

Every package is a potential attack vector. We know this, it’s why we minimize our production containers.

A production Go service might have 15 to 25 packages. The Go runtime, some system libraries, a few utilities.

The ubuntu-latest runner has 1,681 packages. That’s somewhere between 50 and 100 times more than our production containers. Each one has the potential to have vulnerabilities.

The math is brutal. If 4.6% of scanned packages have CVEs, and we extrapolate that to all 1,681 packages, we’re looking at roughly 77 vulnerable packages in the full environment. Even if the real number is half that, it’s still 38 packages with known exploits.

In production, one CVE gets immediate attention. We patch, test, deploy. Having 38 or 77 known vulnerabilities would be a critical incident.

grusPlan

The big problem

We treat production extremely delicately with tightly audited layers, and a fast reflex for patching a critical CVE. Our builds have access to source, secrets, and signing keys, so they should deserve equal or stricter treatment.

Here’s what really bothers us. Those 9 high severity vulnerabilities are known, they’re in a public CVE database (published by security researchers).

Yet, they’re still there on the runner.

Maybe GitHub will update the image next week. Maybe next month. Maybe they already did and we tested an older image. The point is, we don’t control the timing, we can’t force an update, we can’t even see what changed without diffing the entire environment ourselves.

In production, we’d be on those vulnerabilities immediately, patch, test, deploy same day if possible. But for builds? We just accept it, we run our CI on whatever GitHub gives us and hope the vulnerabilities don’t matter.

How we collected this data

We ran this inventory on an actual ubuntu-latest runner using GitHub Actions. Package counts came from the native package managers. APT packages from dpkg, Python from pip, Ruby from gem, and so on.

Vulnerability scanning used the OSV.dev API (Google’s Open Source Vulnerability database). The scan covered Python, Ruby, and NPM packages, but couldn’t handle APT or Conda packages due to scanner limitations.

Total scan time was about 4 seconds. Out of 348 packages scanned, 16 had CVEs. Totally, we found 63 vulnerabilities.

We only scanned 348 of the 1,681 packages. The remaining 1,333 packages might be perfectly clean, or they might have dozens more vulnerabilities. We can’t tell because the scanner doesn’t support them, and we’re too lazy to write our own scanner for APT packages.

We want control

Currently we run our builds with GitHub runners since we initially created our product with them, but moving away from them is a priority.

We aren’t moving away because they’re bad, but because they’re wrong for us. We’re security focused and we want a minimum footprint. We want to know exactly what’s in our environment. We want control over updates and we want to keep the same standards for builds that we have for production.

GitHub runners optimize for convenience. They give you everything so you don’t have to think about what you need. That’s great for getting started quickly but terrible for security.

You can build your own images, it takes more work, but the security benefits are massive.

If you wouldn’t run 1,681 packages in production, don’t build on it.

Our build environment is production. We’re treating it that way.