Contents
dstack’s post-mortem of an October incident includes a root cause that is easy to reproduce in your own service if it talks to Docker from Rust. A public /logs/<container> endpoint passed the container name to bollard, the Rust Docker API client, without validating it. Bollard put the name into the request path without escaping it, so a crafted name turned a log request into a container-inspect call. The inspect response includes Config.Env, which is where many deployments keep their secrets.
The library fix was merged into bollard on October 15. When I checked on November 3, crates.io listed 0.21.1, published August 16, as the newest release. As far as I can tell from those two sources, every released version still has the bug, and only a git dependency on master has the fix.
Why the earlier fix was not enough
The PR describes an earlier change (#737) that stopped ? and # in a name from being parsed as a query or fragment. The path was then resolved with Url::set_path, and that left two other inputs working: / and dot segments. The PR gives these examples:
| Call | Request sent |
|---|---|
remove_container("../images/foo", None) | DELETE /v1.xx/images/foo |
inspect_container("%2e%2e/images/foo", None) | GET /v1.xx/images/foo/json |
logs("foo/json?", ..) on released versions | GET /containers/foo/json?<options> |
The second row is the one that surprises people. The percent-encoded %2e%2e is decoded by the url crate into .. and then normalized, so a filter that looks for a literal ../ in the string never sees it. The third row is the dstack shape: the log call becomes an inspect call, and the options meant for logs end up in the query string of a different endpoint.
The first row matters more than the leak. DELETE against an image path from a call named remove_container means a name you thought was scoped to containers can reach other resource types. What an attacker gets depends on which bollard methods your code calls with input it did not construct, and on what the Docker socket’s owner is allowed to do. If the service talks to the Docker socket, the blast radius is whatever that socket’s permissions allow.
What the fix does
According to the PR, identifiers are now percent-encoded as a single path segment by a new uri::escape_segment. Image references and plugin names use uri::escape_reference, which also leaves / alone because those values legitimately contain slashes. The request builder returns a new Error::InvalidRequestPathError if the decoded path contains a . or .. segment, and a second commit moved that check to the percent-decoded path so x/../images/y is rejected even with the slash escaped as %2F.
Two design choices there are worth copying. Encoding handles the identifier. The dot-segment rejection is a separate check on the final path, because encoding alone does not stop a client library or proxy further along from normalizing the result. The PR covers container, network, volume, exec, service, secret, config, node and task identifiers, plus image and plugin names, so it is not limited to the logs route.
What to do until a release exists
I have not tested any of these against a running Docker daemon. They follow from the PR text.
- Search your code for bollard calls whose first argument comes from a request, a queue message or a database row. Container name, exec ID and image reference are the likely ones.
- Validate before the call, not after. An allowlist regex such as letters, digits,
_and-rejects/,%,?and dot segments outright. Derive the allowlist from how you create your containers, not from what Docker accepts. - Prefer passing the ID that Docker returned at create time over a name a user typed. A 64-character hex string has no way to carry a path.
- If you want the fix now, pin the git revision of bollard that contains commit
8f7b818and plan to move back to crates.io once a release carries it. Check that the commit is in your pinned revision, since I only read the PR page and not the repository. - Keep the socket out of anything reachable by untrusted input. The dstack incident also involved an unauthenticated host management API and shared admin tokens. Escaping fixes one hop in a chain that needed several.
What I could not confirm
The dstack write-up says a CVE ID is pending and I did not find one. I also could not check whether a bollard release is scheduled. The PR shows only that the change is on master. dstack’s own fixes are in dstack#1442 for v0.6.1 and dstack#1443 for v0.5.12, which suggests they patched their route rather than waiting for the library.
The general rule outlives this bug. A typed client method named inspect_container(name) reads like a function over a string, but it is a template for a URL, and the string decides which URL. Any client library that builds paths with format! needs the same two layers: encode on the way in, and refuse dot segments on the way out.
Sources
- dstack post-mortem, October 2026 guest-agent vulnerability, published October 19, 2026. Source for the incident, affected image versions and the
Config.Envexposure. This is the vendor’s own account. - bollard pull request #832, merged October 15, 2026. Source for the examples, function coverage and fix design. I read the PR page, not the diff.
- bollard on crates.io, checked through the crates.io API on November 3, 2026: latest version 0.21.1, published 2026-08-16.
- bollard releases: v0.21.1 notes mention a
ureqdependency change, with no path escaping entry.