Maven supply chain security: the missing lockfile and mirror-based gating
Maven is the oldest mainstream package manager still in heavy use, and its supply-chain model shows its age in one specific way: by default, a Maven build has no lockfile. That single fact shapes the whole threat model.
This post covers the Maven install/build-time surface, the reproducibility gap left by the missing lockfile, and how a Central mirror intercepts artifact downloads.
The build is the execution surface
Maven doesn't run an "install script" when it downloads a dependency. The execution risk is the build itself: Maven plugins are ordinary Java that runs during mvn package/install/verify. A project's POM binds plugins to lifecycle phases, and a malicious or compromised plugin executes with the build's privileges. Transitive plugin and dependency resolution means the set of code that can run during a build is larger than the dependencies a developer explicitly named.
The missing lockfile
This is Maven's defining supply-chain weakness. There is no native, default lockfile. A POM declares dependencies — often with version ranges, and SNAPSHOT dependencies that resolve to whatever the repository currently serves. Two builds of the "same" project, days apart, can resolve different bytes, and nothing in stock Maven records or enforces a pinned set.
Mitigations exist but are opt-in:
- Dependency lock plugins and
mvn dependency:tree-derived pins for reproducibility. verification-metadata.xml(shared with Gradle's verification model) records a SHA-256 per artifact, giving real integrity when a team adopts it.- Pinning exact versions and avoiding SNAPSHOTs in production builds.
Because pinning is optional, dependency-confusion and "resolve drifts to a malicious newer version" attacks have more room in Maven than in lockfile-first ecosystems.
Maven Central is single-host — gating is a mirror
Maven Central (repo1.maven.org) serves both POM metadata and the .jar/.pom artifacts from one host. That makes a gate a straightforward Central mirror: front repo1.maven.org, and the artifact path the enforcer recognizes (…/<group>/<artifact>/<version>/<artifact>-<version>.jar) is identical on the way in and out.
Maven's own mirror mechanism lives in settings.xml (<mirror><mirrorOf>*</mirrorOf>). The catch is not clobbering a developer's existing settings.xml — auth, profiles, private repos. Maven 3.9+ solves this cleanly: MAVEN_ARGS=-gs <file> adds a global settings file whose mirror is merged additively with the user's settings, rather than replacing them.
How install-time gating fits
With the gate set as an additive Central mirror:
- OSV lookup against the
Mavenecosystem (group:artifact coordinates). - Threat-feed match for known-malicious coordinates.
- SHA-256 verification of each
.jaragainstverification-metadata.xml, when the project has adopted Gradle/Maven dependency verification.
And because plugins run code during the build, the gate pairs with an OS sandbox that contains mvn and restricts its network to the gate.
With Veln, veln safe mvn package sets MAVEN_ARGS=-gs to an additive gate-mirror settings file (Maven ≥3.9), scores every .jar, and runs the build inside an OS sandbox — your ~/.m2/settings.xml is left intact. veln verify reads gradle.lockfile and verification-metadata.xml for CI and pre-commit gating.
The practical takeaway
Maven's biggest supply-chain risk isn't an exotic attack — it's the default of no pinning. Adopt a lockfile mechanism and verification-metadata.xml, pin versions, and keep SNAPSHOTs out of release builds. Then layer interception on top: integrity verification confirms the bytes, OSV and threat-feed flag the known-bad, and a contained build limits what a malicious plugin can reach. The reproducibility gap is yours to close; the interception covers the rest.