Maven, Go and Cargo package security
The settings that keep JVM, Go and Rust builds on known packages.
What each setting in settings.xml, the go command and .cargo/config.toml does, what it misses, and where a package firewall fits.
Questions and answers
How do malicious packages reach a Maven, Go or Cargo build?
The same ways they reach an npm one: a new release from a hijacked maintainer account, a lookalike name, a public package that takes the name of a private one, or a dependency that is pulled in by another dependency. Cargo adds one more: a crate’s build.rs is compiled and run just before the crate is built, so a malicious crate can run code on the build machine without anything importing it. That is how proc-macro1 ran its payload in August 2026.
Each of the three tools has settings that narrow these paths. They are below, checked against the official documentation, with what each one does not catch.
Which Maven settings help?
Send every request to one repository
Without a mirror, Maven queries repositories in order “until one returns a valid result”: global and user settings.xml, the project POM, its parent POMs, the standard repositories in the Super POM, then repositories declared in the POMs of your dependencies. A dependency can bring its own repository into your build that way.
A mirror whose mirrorOf is * sends every one of those requests to a single URL:
<!-- ~/.m2/settings.xml -->
<mirrors>
<mirror>
<id>internal</id>
<url>https://repo.yourco.example/maven/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
The Maven guide says this “is most useful when using an internal company repository” that proxies external requests. external:* mirrors everything except localhost and file repositories, and !id excludes one repository by its id.
Fail the build on a bad checksum
checksumPolicy sets what Maven does with a missing or incorrect checksum, separately for releases and snapshots: ignore, warn or fail. On the command line, -C (--strict-checksums) fails the build when checksums do not match.
<releases>
<checksumPolicy>fail</checksumPolicy>
</releases>
The checksum files are deployed alongside the artifact by whoever published it, so this catches corruption and tampering on the way to you. A malicious artifact published with correct checksums passes.
Own your groupId on Maven Central
A groupId such as com.yourco is a namespace on Maven Central, and Sonatype only grants publishing rights under a domain-based namespace after you prove ownership with a DNS TXT record. Registering yours means nobody else can publish under your internal group names there.
Which Go settings help?
Keep go.sum and the checksum database on
go.sum holds a SHA-256 hash for every module version your build uses. When the go command downloads a module whose hash differs from the one in go.sum, it reports a security error and does not add the file to the module cache. When go.sum has no line for a module, the command checks the hash against the checksum database at sum.golang.org, a transparent log run by Google. The Go documentation says this “makes untrusted proxies possible since they can’t serve the wrong code without it going unnoticed.”
GOSUMDB=off turns that check off for every module: the go command “will not authenticate downloaded modules unless they’re already in go.sum”. If private modules fail the lookup because the checksum database has never seen them, set GONOSUMDB (or GOPRIVATE) to their path prefixes instead; that skips the check for those modules only. Leave GOSUMDB at its default for public modules, including when GOPROXY points at a package firewall.
Both checks confirm that you get the same bytes as everyone else for a given version. A malicious version that was published normally gets a valid entry like any other.
Mark private modules with GOPRIVATE
go env -w GOPRIVATE=corp.example.com
GOPRIVATE takes glob patterns of module path prefixes and is the default for GONOPROXY and GONOSUMDB. Matching modules are fetched directly from their repositories and are never sent to the public proxy or checksum database. If you run a proxy that serves every module, set GONOSUMDB for your private prefix instead and leave GONOPROXY unset.
Know when GOPROXY falls back
GOPROXY is a comma-separated list, and its default is https://proxy.golang.org,direct. The go command moves to the next entry only when a proxy answers 404 or 410. Any other error stops the download, and the Go documentation gives the example of a proxy answering 403 for a module with known security vulnerabilities. A proxy that refuses a module therefore has to answer with something other than 404 or 410, or the list will fall through to direct (with | instead of ,, it falls back on any error).
Do not let builds edit go.mod
Since Go 1.16, the go command acts as if -mod=readonly were set: if go.mod needs changes, it reports an error instead of making them. Setting GOFLAGS=-mod=readonly in CI keeps that true even when a vendor directory is present. -mod=vendor builds from the vendor directory and does not use the network or the module cache. go mod verify checks that the modules in your cache have not been modified since they were downloaded.
Which Cargo settings help?
Commit Cargo.lock and build with —locked
Cargo.lock records the exact version and source of every dependency. --locked makes Cargo exit with an error if the lockfile is missing or would change, and the Cargo docs suggest it for CI. --frozen is --locked plus --offline.
cargo install ignores the Cargo.lock inside a package by default and resolves dependency versions again, possibly to releases newer than the package. Use cargo install --locked to keep the published lockfile.
Replace crates.io with your mirror or a vendor directory
Source replacement in .cargo/config.toml sends crates.io traffic somewhere else:
[source.crates-io]
replace-with = "internal"
[source.internal]
registry = "https://crates.yourco.example/index"
A replacement may be a registry, a local registry or a directory. cargo vendor copies every crates.io and git dependency into a directory and prints the configuration that points Cargo at it. Cargo assumes a replacement source contains exactly the same code as the original.
One gap: cargo install reads configuration from $CARGO_HOME/config.toml, not from the project, unless you install with --path. A replacement that only lives in the repository does not apply to cargo install.
Treat build.rs as code that runs on your machine
Cargo compiles a package’s build.rs and runs it just before building the package, and the script “may perform any number of tasks”. Its intended uses include building a bundled C library or generating code, but nothing limits it to those. Build untrusted dependency updates where a build script can do little harm, and read new build scripts in a dependency diff.
Audit the lockfile
cargo-audit checks Cargo.lock against the RustSec Advisory Database. cargo-deny’s sources check limits where crates may come from: allow-registry defaults to crates.io, and setting unknown-registry and unknown-git to deny fails the check for any crate from another registry or git repository. Both default to warn.
What do these settings not cover?
They check that you got the bytes you expected from the place you expected. None of them judges the bytes. A malicious release published normally has a valid checksum, a valid go.sum line on first use and a valid Maven Central listing. A lockfile protects versions already locked; adding or updating a dependency resolves it again. And each setting lives on one machine or in one repository, where it can be forgotten or switched off.
Where does a package firewall fit?
A package firewall sits on the one URL your settings.xml mirror, GOPROXY and Cargo source replacement point at, and decides on each package before it is served. For Maven, Go and Cargo, Chainsaw runs its typosquat matchers and malware feed, a cooldown policy rule (cooldownDays), seeded in monitor mode, that quarantines versions published inside your window once you set it to block, and a reserved-namespaces setting that refuses public packages under names you declare. On crates.io it also checks build scripts: the default rule on install scripts that fetch remote URLs refuses onering 1.4.1, whose build.rs runs curl. In our re-run of 58 attacks from 2026, the default typosquat rule refused proc-macro1 on its first download, so the three crates that depended on it would have failed to build for anyone installing through Chainsaw.
Keep the settings above as well. A firewall decides what is served; checksums, go.sum and --locked make sure the build uses what was served. What a package firewall is and how it works.
Sources
- Apache Maven, Setting up multiple repositories, Using mirrors for repositories, Settings reference:
checksumPolicyand command-line options. - Sonatype, Maven Central namespace registration.
- Go, Go Modules Reference: authenticating modules and
go.sum, private modules,go mod verifyand build flags. - The Cargo Book,
cargo buildmanifest options,cargo install, source replacement,cargo vendor, build scripts and Cargo.toml vs Cargo.lock. - RustSec Advisory Database and cargo-audit; cargo-deny, sources check configuration.
- JFrog Security Research, Compromised Rust crates on crates.io silently execute malware at build time, August 2026.
One URL per package manager
Apply the same checks to Maven, Go and Cargo installs
Start a free org in monitor mode and see which Maven, Go and Cargo packages Chainsaw would refuse, then turn rules on one at a time.