Can someone poison our shared build cache?
Anyone who holds the write login can change what every other build restores. FosterStack Cache does not stop that holder. It lets you give everyone else a read-only login, and in our runs the server refused their writes.
The short answer
The server has two logins. The write login lets a client store and replace entries. The read-only login lets a client only read them.
- With the write login, yes. In our runs one plain HTTP request replaced a real entry. A fresh build from untouched source then restored the replaced output.
- With the read-only login, no. Its writes and deletes were refused with a 403, and the stored entry stayed the same.
- With no login, nothing. Reads and writes were refused with a 401.
- The server stored whatever the write login sent. That was a different valid entry and 200 random bytes. The random bytes broke the next build.
- Turning off uploads in Gradle (
isPush = false) does not protect the cache. With it set, the build uploaded nothing, but the write login could still write entries with a direct request.
Is a remote build cache safe? Who can write to it
Two kinds of failure, as background and not a test result:
- Down, empty or unreachable. Builds run slower. Nothing is lost and nothing is wrong. A build cache is only slower in this case. See what happens to my build when the cache server is down.
- Serving wrong files. If something untrusted can write to the cache, it can change what later builds use as task outputs. This is a different class of problem, and it is the one to plan for.
What to do
Our advice, not a test result:
- Turn on Basic Auth anywhere untrusted parties could reach the port.
- Give the write login to as few machines as you can. Usually that means CI only, plus the developers you already trust to change your code.
- Give everyone else the optional read-only login. That includes laptops. The setup is on let CI write, developers only read, and the CI wiring is on Jenkins and CI build cache.
- Keep builds of untrusted code on the read-only login. An example is a pull request from a fork.
- Do not count on
isPush = false. It only controls whether your own build uploads. The login is what the server enforces. - Use HTTPS, on a network you control. Gradle sends the login with its requests.
- If you doubt an entry, clear the cache. See how to clear or reset a build cache.
FosterStack Cache has one write login and one read-only login. There is no per-person identity: a write login is shared by whoever holds it, so the login alone does not say which writer did what. We did not test whether the server records who wrote an entry.
How we checked
We ran a real FosterStack Cache server with a write login and a read-only login. We built a project, then tried each login against the stored entries with plain HTTP requests.
| Who | What they did | What happened |
|---|---|---|
| Read-only login | Fresh build, nothing changed | All four compile tasks came from the cache |
| Write login | Put a different valid entry over the core module's entry | HTTP 201, entry replaced |
| Read-only login | Fresh build from the same untouched source | Core came from the cache. Its class file held text that is not in the source |
| Write login | Put 200 random bytes over another entry | HTTP 201. The next build failed: Not in GZIP format |
| Read-only login | Put bytes over an entry, and over a new key | HTTP 403 both times. Entry unchanged, nothing stored |
| Read-only login | Delete an entry, and read one | Delete: HTTP 403, entry still there. Read: HTTP 200 |
| No login | Put, and read | HTTP 401 both times. A wrong password: HTTP 401 |
Write login, isPush = false | Build with a changed file | Nothing uploaded. Then a direct put of a new key: HTTP 201 |
Read-only login, isPush = true | Build with a changed file | Build succeeded. The upload got a 403 and nothing was stored. Gradle then switched the remote cache off for the rest of that build (see can feature branches share the build cache with main) |
What the replaced entry did: the project's source says core-1-. The class file restored from the replaced entry contained core-1-TAMPERED-. Gradle did not complain.
What the damaged entry did: Gradle stopped with Failed to load cache entry ... Could not load from remote cache: Not in GZIP format. It did not skip the entry and compile instead.
Setup: FosterStack Cache 0.2.1 and Gradle 9.8.0 on a Mac (arm64), Temurin 21.0.12.1, all on one machine over plain HTTP on the loopback address. The project had four modules, 12 small classes each. Every build used a fresh copy of the project and a new empty Gradle home, with Gradle's local cache off, so only the server could supply results.
Limits: one server and one small project. Gradle only, not Maven. No HTTPS. We tested what each login can do to an entry. We did not test how anyone would get a login.
Related: security policy.