DevSecOps

Please Build System Security Review

A hands-on security review of Please, the open-source Bazel-inspired build system, including sandbox behavior, BUILD rules, and supply chain trade-offs.

Shadab Khan
Senior Security Engineer
6 min read

Please (the plz command) is one of those projects that quietly does a lot right and gets less recognition than it deserves. Thought Machine open-sourced it in 2016 as a Bazel-inspired, Go-implemented build system meant to be easier to adopt than Bazel. I spent six weeks in the first half of 2024 auditing a Please 17.3.0 deployment for a banking client, and this post covers what that audit surfaced on the supply chain front.

I am not comparing Please to Bazel on features. The comparison that matters for supply chain is: does Please's security model hold up under adversarial assumptions, and what do you need to configure to get there?

What Please Provides Out of the Box

Please builds are declared in BUILD files using a Python subset (not Starlark -- Please has its own dialect, close enough to Bazel's that the difference is mostly cosmetic). The plz binary is statically linked Go; there is no JVM, no Python runtime dependency, and no separate daemon in the classical sense (Please has a daemon mode, but the CLI works fine without it).

The sandbox is the first place to look. Please sandboxes actions using Linux namespaces when available. On a recent kernel with user namespaces enabled, an action runs in its own mount, PID, user, and network namespace. On macOS, Please falls back to path restriction only -- no true sandbox. On Windows, sandbox support is best described as aspirational.

The default sandbox config in .plzconfig:

[build]
path = /usr/local/bin:/usr/bin:/bin
hashcheck = true
sandbox = true

[sandbox]
tool = /usr/bin/please_sandbox
namespace = all

hashcheck = true is critical and, fortunately, default. This causes Please to verify the hash of every input file before running an action. If a file on disk has been tampered with post-checkout, the build fails before running untrusted code. Disable this at your peril; I have seen teams set it to false to work around a CI performance issue and effectively open a local tampering channel.

Subrepos: Please's Third-Party Model

Please's equivalent of Bazel's http_archive or Bzlmod's bazel_dep is the subrepo. Subrepos are defined in BUILD files at the repo root:

http_archive(
    name = "protobuf",
    urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protobuf-25.1.tar.gz"],
    hashes = ["sha256: 9bd87b8280ef720d3240514f884e56a712f2218f0d693b48050c836028940a42"],
    strip_prefix = "protobuf-25.1",
)

pip_library(
    name = "requests",
    version = "2.31.0",
    hashes = ["sha256: 942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"],
)

The hashes field is enforced. Please refuses to build if the downloaded file does not match, and the error is a hard failure. This is stricter than Bazel's legacy http_archive (where sha256 was optional) and comparable to Bzlmod's integrity enforcement.

The concern is that hashes is per-declaration. There is no central registry like Bazel Central Registry, no automatic mirroring, and no default integrity database. If a developer adds a new http_archive without hashes, the build succeeds (with a warning). We added a pre-commit hook for the banking client that greps for http_archive declarations missing the hashes attribute and fails the commit.

pip_library and the Python Supply Chain

Please's pip_library rule resolves a single Python package and its transitive dependencies. Unlike Pants or Bazel's pip_parse, Please does not produce a lockfile. Each pip_library declaration is independent, and resolution happens at build time with pip as the backend.

This is the weakest part of Please's supply chain story. If two pip_library declarations require incompatible versions of a transitive dependency, you get non-deterministic resolution. If a transitive dependency changes upstream (e.g., a patch release of urllib3), the build output changes without any explicit PR.

The workaround is to pin every transitive dependency explicitly:

pip_library(
    name = "requests",
    version = "2.31.0",
    hashes = ["sha256: ..."],
    deps = [
        ":certifi",
        ":charset_normalizer",
        ":idna",
        ":urllib3",
    ],
)

For the banking client, we generated 900+ explicit pip_library declarations from a master requirements.txt using a script. It is ugly but it gives you lockfile-equivalent behavior.

Please 17.x roadmaps a proper lockfile rule. As of this writing, it is not merged.

go_module and the Go Supply Chain

The Go story is better. go_module delegates to the Go toolchain, which enforces go.sum integrity natively. Please's go_module rules honor the checksums in go.sum, so if you are using Go, you inherit Go's supply chain model for free.

go_module(
    name = "echo",
    module = "github.com/labstack/echo/v4",
    version = "v4.11.4",
    deps = [
        ":echo_middleware",
    ],
)

Please verifies the module content against go.sum via go mod download -x. This is the standard Go trust path: the module proxy (by default, proxy.golang.org) serves an immutable module that can be verified against the checksum database (sum.golang.org). Please does not second-guess this.

Please Daemon and IPC Surface

Please has a daemon mode (plz-watch, plz --daemon) that keeps a long-running process to speed up repeated builds. The daemon speaks over a Unix socket at /tmp/plz_<repo_hash>.sock. Any local process that can reach that socket can submit builds.

From a supply chain perspective, this is a local privilege escalation surface if your developer workstation has shared access (common in shared-lab setups and less common but still seen on misconfigured shared-ssh hosts). Mitigations:

  • Set umask 0077 before starting the daemon so the socket is user-readable only.
  • Disable daemon mode in CI. The ephemeral build process is a cleaner trust boundary.
  • Do not run Please as root or with elevated privileges.

Reproducibility

Please builds are generally reproducible. The audit for the banking client ran ten rebuilds of the same commit on three different developer machines and a CI worker, and the output hashes matched for 93% of targets. The non-reproducible 7% fell into familiar categories: timestamps in JARs, Docker image timestamps, and Go build IDs.

Fixes in .plzconfig:

[go]
buildflags = -trimpath -ldflags=-buildid=

[docker]
reproducible = true
source_date_epoch = 1704067200

The source_date_epoch setting in [docker] is Please-specific and not widely documented. It sets the timestamp for every file in Please-built OCI images, which is what you need for reproducible container builds.

Remote Caching and Execution

Please has its own remote cache protocol (plz-rcache) that predates REAPI. It works but has a small ecosystem. Please 16+ added REAPI support so you can use BuildBuddy or BuildBarn as a backend, but the Please-native cache is still the default in most deployments.

For supply chain, the concern is that plz-rcache is less battle-tested than REAPI. It lacks some of the integrity checks REAPI has baked in. If you run Please at scale with a remote cache, consider switching to a REAPI backend:

[remote]
url = grpcs://buildbarn.internal:8980
instance = please
num_executors = 200
upload_dirs = true

How Safeguard Helps

Safeguard ingests Please BUILD files and the implicit dependency graph from plz query graph to produce CycloneDX 1.5 SBOMs that capture every subrepo, pip_library, and go_module with integrity context. Policy gates can block PRs introducing http_archive declarations without the hashes attribute or pip_library entries without explicit transitive pinning. For teams running the Please daemon, Safeguard ties remote-cache action digests to source commits to flag cache drift. The combination addresses Please's genuine supply chain weaknesses -- especially around Python dependency resolution -- with the enforcement and visibility the tool does not yet provide natively.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.