Supply-Chain Security for JVM Builds
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Ant 1.10.18 with Apache Ivy 2.6.0, Apache Maven 3.10.0 (3.9.16 where it differs, and
Maven 4.0.0-rc-7, a release candidate, where named) and Gradle 9.8.1 (8.14.6 where it differs). Every example marked as run
was executed on JDK 26 on 2026-10-16 in a scratch directory; output that comes from a run is shown as it was printed, with
the long absolute path of the scratch directory shortened to <scratch> and the home directory to <home>. Anything that could not be run here is labelled
"not run here" in the text. All identifiers (CVE numbers, advisory ids, versions and dates) were looked up on the vendors'
own pages on the same day.
A build tool executes code from several parties before it compiles a single line of yours: the tool itself (and, for
Gradle and Maven, its wrapper), the build files, the plugins, the libraries downloaded for the compile and runtime
classpaths, and the repositories that serve all of them. A compromise anywhere in that chain becomes code running with
the permissions of the developer or the CI job. This page follows the chain in the order a build meets it: trust model,
tool distributions, wrappers, dependency integrity, credentials, SBOMs and, last, the security releases of 2026. The
running scenario is tidepool, described on the section landing
page; the dependency used in the examples is org.apache.commons:commons-csv:1.14.1. For how each tool resolves
dependencies in the first place see
Dependency Management Fundamentals and
Repositories & Publishing Across Tools.
The trust model: a build file is code
None of the three tools sandboxes a build. A build.xml, a pom.xml (through its plugins) and a build.gradle(.kts) can
all run arbitrary code on the machine that builds them, so each tool’s security position starts from the same sentence:
only build projects whose build files, dependencies and repositories you trust.
| Tool | Stated position | What follows from it |
|---|---|---|
Ant |
A build file can run any task, including |
Reading a |
Maven |
The security page states that Maven assumes you trust the |
Plugins bound to the lifecycle run during |
Gradle |
The security best practices advise against running |
Check the wrapper before running it (see Validating the wrapper JAR in CI), review the scripts, and use dependency verification so that what the scripts download is pinned. |
Source: Maven: Security Model
The defences on the rest of this page are about tampering (a different file than the one you meant to get) and drift (a different file from yesterday’s), not about malicious-by-design code you decided to trust. The flow below is the common shape; each tool implements the stages with different mechanisms.
The weakest stage is the first one: a checksum served by the same server as the file proves the download was not corrupted, not that the server is honest. The checksum has to come over a different channel (the project’s web site for a tool, a file committed to your repository for a dependency) to protect against a compromised server. Every section below says which channel the checksum travels over.
Verifying tool distributions
Ant and Maven: SHA-512 and PGP
The Apache download pages publish, next to every archive, a .sha512 file, an .asc PGP signature and a KEYS file with
the release managers' public keys. Both projects tell you to verify the signature and say the checksum is the weaker of the
two checks; Ant’s page adds that you should take the KEYS and .asc files from the main distribution directory rather
than from a mirror.
Ant 1.10.18 and Maven 3.10.0 publish the .sha512 file as the bare hexadecimal digest, with no file name, so the
shasum -c mode needs the name added. The check below was run against the Maven 3.10.0 archive that this series downloaded
and verified, and then against a copy with one byte changed:
echo "$(cat apache-maven-3.10.0-bin.tar.gz.sha512) apache-maven-3.10.0-bin.tar.gz" | shasum -a 512 -c -
cp apache-maven-3.10.0-bin.tar.gz tampered.tar.gz
printf 'X' | dd of=tampered.tar.gz bs=1 seek=1000 conv=notrunc
echo "$(cat apache-maven-3.10.0-bin.tar.gz.sha512) tampered.tar.gz" | shasum -a 512 -c -
apache-maven-3.10.0-bin.tar.gz: OK
tampered.tar.gz: FAILED
shasum: WARNING: 1 computed checksum did NOT match
The PGP step, as the Ant download page describes it, is gpg --import KEYS followed by gpg --verify of the .asc file
(Ant’s own KEYS file is also inside its binary distribution, and gpg 2.2.41 was available here). The signature check
itself was not run here, because this series downloaded only the archives and checksums. A signature proves the archive
was produced by a holder of one of the listed keys, so the remaining question is how you came to trust KEYS: download it
over HTTPS from downloads.apache.org, and compare key fingerprints with the Apache web of trust when the stakes justify
it.
Source: Maven: Downloading Apache Maven
Gradle: SHA-256
Gradle publishes a SHA-256 checksum for each distribution on its release-checksums page and next to the file on
services.gradle.org. The checksum file for 9.8.1 used here is gradle-9.8.1-bin.zip.sha256; the value of the distribution
this series downloaded matched the one published for 9.8.1:
shasum -a 256 gradle-9.8.1-bin.zip
dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f gradle-9.8.1-bin.zip
For the wrapper, Gradle ships the checking as a feature: the next section shows it. Gradle’s security best practices list both "validate the Gradle distribution SHA-256 checksum" and "validate the Gradle Wrapper on every upgrade" as recommended practices.
Source: Gradle: release checksums
Distributions ship their own SBOMs
The Ant 1.10.18 binary distribution carries a CycloneDX SBOM for every JAR in lib, in JSON and XML next to the JAR, and
the download page lists .cdx.json files next to each archive. The distribution unpacked here has 26 such JSON files.
ls apache-ant-1.10.18/lib | grep -c cdx.json
python3 -c "import json; d=json.load(open('apache-ant-1.10.18/lib/ant.cdx.json')); print(d['bomFormat'], d['specVersion']); print(d['metadata']['component']['name'], d['metadata']['component']['version'])"
26
CycloneDX 1.6
ant 1.10.18
Source: Ant: Binary Distributions
Wrappers
A wrapper is a script and a small binary committed to your repository that downloads a specific tool version. It removes the "which Gradle or Maven did you install" problem, and it adds an attack surface: the files are executable code in your repository, and they download an archive from a URL. Ant has no wrapper; its installation is the unpacked archive, verified as above. The details of each wrapper are in Gradle Wrapper and Maven Wrapper; this section covers only the verification.
Gradle: distributionSha256Sum and the wrapper JAR
gradle-wrapper.properties accepts distributionSha256Sum. When it is set, the wrapper hashes the downloaded archive and
refuses to continue on a mismatch. The wrapper task writes it when you pass --gradle-distribution-sha256-sum (a task option
confirmed with gradle help --task wrapper on 9.8.1; the other options listed are --gradle-version,
--gradle-distribution-url, --distribution-type, --validate-url, --network-timeout, --retries and
--retry-back-off-ms).
The example uses a wrapper directory whose distributionUrl points to the local copy of the 9.8.1 archive (to avoid a
second download) with a deliberately wrong checksum:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=0000000000000000000000000000000000000000000000000000000000000000
distributionUrl=file\:<scratch>/tools/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=false
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
GRADLE_USER_HOME=$PWD/guh ./gradlew --version
Fetching distribution.
Downloading file:<scratch>/tools/gradle-9.8.1-bin.zip
..............10%..............20%...............30%..............40%...............50%..............60%...............70%..............80%...............90%..............100%
Exception in thread "main" java.lang.RuntimeException: Verification of Gradle distribution failed!
Your Gradle distribution may have been tampered with.
Confirm that the 'distributionSha256Sum' property in your gradle-wrapper.properties file is correct and you are downloading the wrapper from a trusted source.
Distribution Url: file:<scratch>/tools/gradle-9.8.1-bin.zip
Download Location: <scratch>/work/supply-chain/guh1/wrapper/dists/gradle-9.8.1-bin/b059gjp0p6cuxvj40w8csa0e5/gradle-9.8.1-bin.zip
Expected checksum: '0000000000000000000000000000000000000000000000000000000000000000'
Actual checksum: 'dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f'
Visit https://gradle.org/release-checksums/ to verify the checksums of official distributions. If your build uses a custom distribution, see with its provider.
at org.gradle.wrapper.Install.verifyDownloadChecksum(SourceFile:2)
at org.gradle.wrapper.Install.lambda$createDist$0(SourceFile:12)
at org.gradle.wrapper.Install.createDist(SourceFile:24)
at org.gradle.wrapper.GradleWrapperMain.lambda$prepareWrapper$0(SourceFile:2)
at org.gradle.wrapper.GradleWrapperMain.main(SourceFile:2)
The official properties for 9.8.1 differ only in the two lines that point at https://services.gradle.org/distributions/
and in validateDistributionUrl=true. distributionSha256Sum protects the archive. It does not protect the other
executable pieces in the repository, gradlew, gradlew.bat and gradle/wrapper/gradle-wrapper.jar, which run before
any of that checking happens. The JAR’s own checksum is published by Gradle, and the 9.8.1 value matched the one in the
scratch project:
shasum -a 256 gradle/wrapper/gradle-wrapper.jar
curl -s https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98 gradle/wrapper/gradle-wrapper.jar
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98
Validating the wrapper JAR in CI
A pull request that touches gradle-wrapper.jar shows as "binary file not shown", which makes it a convenient place to hide
code. The gradle/actions project provides two ways to check every gradle-wrapper.jar in the repository against the
SHA-256 values of official Gradle releases (including look-alike file names built with homoglyphs). Since v4 the
setup-gradle action does it on every run, so a separate step is only needed when you do not use that action. The latest
release of the actions when this page was checked was v6.4.0 (2026-09-28). The workflow below is not run here; the
action names and the v6 tag follow the project’s own documentation:
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: gradle/actions/setup-gradle@v6 # validates every gradle-wrapper.jar before building
- run: ./gradlew build
Maven: distributionSha256Sum in maven-wrapper.properties
The Maven Wrapper (plugin 3.3.4, the version on Maven Central on the check date) generates
.mvn/wrapper/maven-wrapper.properties. The file generated here with wrapper:wrapper -Dmaven=3.10.0 contained only the
download location, no checksum:
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip
The generated mvnw script reads an optional distributionSha256Sum and compares it with sha256sum or shasum -a 256.
Adding the property is a manual step (the value to use is the SHA-256 of the zip; neither the Maven download page nor Maven Central
publishes one, both publish SHA-512, so compute it once from an archive you verified with the SHA-512 and PGP steps above).
The run below used a local zip of the verified distribution so that nothing was downloaded; first with a wrong value, then
with the right one. MAVEN_USER_HOME points the wrapper’s download cache to the scratch directory:
MAVEN_USER_HOME=$PWD/muh ./mvnw -v # distributionSha256Sum=000...000
MAVEN_USER_HOME=$PWD/muh ./mvnw -v # distributionSha256Sum=4136dc58...bfa7 (the zip's real SHA-256)
Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.
If you updated your Maven version, you need to update the specified distributionSha256Sum property.
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: <scratch>/work/supply-chain/muh/wrapper/dists/apache-maven-3.10.0/b5eb15c0
Java version: 26, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home
Default locale: es_ES, platform encoding: UTF-8, time zone: Europe/Madrid
OS name: "mac os x", version: "27.0.1", arch: "aarch64", family: "mac"
Everything said above about the unprotected pieces applies to Maven too: mvnw is a shell script that runs before the
checksum is checked, so review changes to it like code.
Dependency integrity
HTTPS only
Plain HTTP lets anyone between you and the repository replace an artifact. Both tools now refuse it by default, and the refusal is visible in a real error.
Maven. Since 3.8.1 (the fix for CVE-2021-26291, below) the default conf/settings.xml contains a blocked mirror for
every external repository that uses HTTP. The relevant part of the file shipped in 3.10.0:
<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>external:http:*</mirrorOf>
<name>Pseudo repository to mirror external repositories initially using HTTP.</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
</mirror>
A POM that declares an HTTP repository (http://repo.example.org/maven2, never contacted because it is blocked) and a
dependency that is only there fails with the same message on Maven 3.10.0, 3.9.16 and 4.0.0-rc-7:
<repositories>
<repository>
<id>legacy</id>
<url>http://repo.example.org/maven2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.example.legacy</groupId>
<artifactId>legacy-lib</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
[ERROR] Failed to execute goal on project tidepool-http: Could not collect dependencies for project org.example.tidepool:tidepool-http:jar:1.0.0-SNAPSHOT
[ERROR] Failed to read artifact descriptor for org.example.legacy:legacy-lib:jar:1.0
[ERROR] Caused by: The following artifacts could not be resolved: org.example.legacy:legacy-lib:pom:1.0 (absent): Could not transfer artifact org.example.legacy:legacy-lib:pom:1.0 from/to maven-default-http-blocker (http://0.0.0.0/): Blocked mirror for repositories: [legacy (http://repo.example.org/maven2, default, releases+snapshots)]
external:http:* (a selector available since Maven 3.8.0) matches all repositories using HTTP except those on
localhost, which is why a local test server on http://localhost is not blocked. Fix the repository (use its HTTPS URL), do not delete the blocker.
Gradle. A repository with an http: URL is rejected unless you opt in with allowInsecureProtocol = true
(isAllowInsecureProtocol in the Kotlin DSL). The build below uses http://localhost:8081/... — Gradle does not exempt
localhost — and no opt-in:
repositories {
maven {
name = "insecure"
url = uri("http://localhost:8081/repository/releases")
}
}
> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all dependencies for configuration ':compileClasspath'.
> Using insecure protocols with repositories, without explicit opt-in, is unsupported.
* Try:
> Switch Maven repository 'insecure(http://localhost:8081/repository/releases)' to redirect to a secure protocol (like HTTPS) or allow insecure protocols.
> For more information, please refer to https://docs.gradle.org/9.8.1/dsl/org.gradle.api.artifacts.repositories.UrlArtifactRepository.html#org.gradle.api.artifacts.repositories.UrlArtifactRepository:allowInsecureProtocol in the Gradle documentation.
repositories {
maven {
name = 'insecure'
url = uri('http://localhost:8081/repository/releases')
}
}
The Groovy build fails with the same message (not re-run separately: the repository declaration is the only difference).
Ant and Ivy. Neither has a policy: the URL you write is the URL used. <get src="http://..."> and an Ivy ibiblio
resolver with an http root work without complaint, so the only defence is to write https and to review every repository
URL in ivysettings.xml and build.xml. Ant 1.10.16 added TLS identity checks for the mail task, and 1.10.18 changed the
ftp default (below); verifying what you fetched (next section) is your build’s job.
Source: Maven 3.8.1 Release Notes
Source: Maven: Guide to Mirror Settings
Checksums from the repository: integrity, not authenticity
Every repository publishes checksums next to its files. For commons-csv-1.14.1.jar on Maven Central the files .sha1, .md5
and .asc exist and .sha256 and .sha512 returned 404 on the check date (other artifacts can differ, so look before relying
on an algorithm). The tools differ in how strictly they use these files:
| Tool | Default behaviour | Stronger setting |
|---|---|---|
Ant ( |
The |
The |
Ivy 2.6.0 |
|
|
Maven (Resolver) |
|
|
Gradle |
Nothing is pinned by default. |
Dependency verification (below), which compares against a file in your repository. |
Both behaviours were checked here against a file: repository whose .jar.sha1 was deliberately wrong: Maven 3.10.0 logged
the ChecksumFailureException as a warning, resolved the JAR and printed BUILD SUCCESS, while Maven 4.0.0-rc-7 stopped
with Checksum validation failed and BUILD FAILURE. So on Maven 3 only --strict-checksums turns these checks into
something that stops a corrupted download. Even then, because a repository that serves a tampered JAR can serve a matching
checksum, repository checksums catch corruption, not tampering. The two layers that follow compare against values the repository does not control.
Ant: verify what you fetch. The target below downloads a JAR and the SHA-1 file Central publishes next to it, and fails the build when they differ. It was run with Ant 1.10.18; the second target changes one byte of a copy to show the failure. The check only proves the download was not damaged, since both files came from the same server:
<project name="verify-download" default="verify">
<property name="central" value="https://repo1.maven.org/maven2"/>
<property name="path" value="org/apache/commons/commons-csv/1.14.1"/>
<property name="jar" value="commons-csv-1.14.1.jar"/>
<target name="verify" description="Downloads a JAR and checks it against the SHA-1 published next to it">
<mkdir dir="build/download"/>
<get src="${central}/${path}/${jar}" dest="build/download/${jar}" skipexisting="true"/>
<get src="${central}/${path}/${jar}.sha1" dest="build/download/${jar}.sha1" skipexisting="true"/>
<loadfile srcFile="build/download/${jar}.sha1" property="expected.sha1">
<filterchain><striplinebreaks/></filterchain>
</loadfile>
<checksum file="build/download/${jar}" algorithm="SHA-1"
property="${expected.sha1}" verifyProperty="sha1.ok"/>
<fail message="SHA-1 mismatch for ${jar}">
<condition><isfalse value="${sha1.ok}"/></condition>
</fail>
<echo message="${jar}: SHA-1 matches (${expected.sha1})"/>
</target>
<target name="tamper" depends="verify" description="Shows what a failing check looks like">
<copy file="build/download/${jar}" tofile="build/download/tampered.jar"/>
<concat destfile="build/download/tampered.jar" append="true">x</concat>
<checksum file="build/download/tampered.jar" algorithm="SHA-1"
property="${expected.sha1}" verifyProperty="tampered.ok"/>
<fail message="SHA-1 mismatch for tampered.jar">
<condition><isfalse value="${tampered.ok}"/></condition>
</fail>
</target>
</project>
verify:
[mkdir] Created dir: <scratch>/work/supply-chain/ant-checksum/build/download
[get] Getting: https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar
[get] To: <scratch>/work/supply-chain/ant-checksum/build/download/commons-csv-1.14.1.jar
[get] Getting: https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar.sha1
[get] To: <scratch>/work/supply-chain/ant-checksum/build/download/commons-csv-1.14.1.jar.sha1
[echo] commons-csv-1.14.1.jar: SHA-1 matches (467354980fade8528b6a9f9bdf7f4f19ab9b3373)
BUILD SUCCESSFUL
Total time: 0 seconds
tamper:
[copy] Copying 1 file to <scratch>/work/supply-chain/ant-checksum/build/download
BUILD FAILED
<scratch>/work/supply-chain/ant-checksum/build.xml:28: SHA-1 mismatch for tampered.jar
Total time: 0 seconds
For a check that does protect you, put the expected value in the repository (a checksums.properties file loaded with
<property file=...>) instead of downloading it from the server you are checking.
Source: Ant: Checksum task
Source: Ant: Get task
Ivy: checksum algorithms. The resolver in ivysettings.xml lists the algorithms to check. With the default, the
-d output of an ivy:retrieve run shows each file checked:
<ivysettings>
<settings defaultResolver="central"/>
<caches defaultCacheDir="${basedir}/build/ivy-cache"/>
<resolvers>
<ibiblio name="central" m2compatible="true" root="https://repo1.maven.org/maven2/"
checksums="sha1"/>
</resolvers>
</ivysettings>
[ivy:retrieve] setting 'ivy.checksums' to 'sha1,md5'
[ivy:retrieve] sha1 file found for https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom: checking...
[ivy:retrieve] central: downloading https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom.sha1
[ivy:retrieve] sha1 OK for https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
Asking Ivy for a stronger algorithm needs the Java algorithm name. With the lowercase checksums="sha256" the resolution
fails:
[ivy:retrieve] java.lang.IllegalArgumentException: Unknown checksum algorithm: sha256
[ivy:retrieve] [FAILED ] org.apache.commons#commons-csv;1.14.1!commons-csv.pom(pom.original): Unknown checksum algorithm: sha256 (28ms)
checksums="SHA-512" (and likewise SHA-256) is accepted, and Ivy 2.6.0 then asks for a file named after the algorithm,
.pom.SHA-512 and .jar.SHA-512. Central has no such files, and a checksum file that is not found is not an error, so the
same ivy:retrieve -d run ends in BUILD SUCCESSFUL with nothing verified (no OK for line in the whole log; trimmed to
the commons-csv lines):
[ivy:retrieve] central: downloading https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom
[ivy:retrieve] HTTP response status: 404 url=https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom.SHA-512
[ivy:retrieve] CLIENT ERROR: Not Found url=https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom.SHA-512
…
[ivy:retrieve] central: downloading https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar
[ivy:retrieve] HTTP response status: 404 url=https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar.SHA-512
[ivy:retrieve] CLIENT ERROR: Not Found url=https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar.SHA-512
…
[ivy:retrieve] 3 artifacts copied, 0 already retrieved (976kB/4ms)
…
BUILD SUCCESSFUL
So against Maven Central the practical options with Ivy are SHA-1 and MD5 from the repository, plus your own <checksum>
verification of the files you retrieve; a SHA-256 or SHA-512 list only helps with a repository that publishes files under
those names. Ivy can also sign artifacts it publishes (the resolver signer attribute, since 2.2); the settings
reference describes signers only for publication.
Source: Ivy: signers
Maven: trusted checksums
The Maven Resolver can pin every resolved artifact to a checksum you hold. The post-processor has two modes: record,
run once in an environment you trust, writes a file of SHA-512 values; and verify, the normal mode, compares every
resolved file with the file and fails the build on a mismatch or, with failIfMissing, on an unknown artifact. Commit the
file to the repository so that the pinning survives a poisoned local repository or repository manager. The feature exists in
Maven 3.9.x (Resolver 1.9) and in 3.10 and 4; this run used 3.10.0.
.mvn/maven.config of the tidepool-engine project (one option per line; the ${session.rootDirectory} expression needs
Maven 3.9 or later):
--strict-checksums
-Daether.checksums.checksumAlgorithms=SHA-512,SHA-1,MD5
-Daether.trustedChecksumsSource.summaryFile=true
-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums/
-Daether.artifactResolver.postProcessor.trustedChecksums=true
-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-512
-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true
The POM declares commons-csv:1.14.1 and pins the compiler and resources plugins. Recording against an empty local
repository (-Dmaven.repo.local points to a scratch directory so that nothing but the build’s own downloads is recorded):
mvn -B -ntp -Dmaven.repo.local=m2repo -Daether.artifactResolver.postProcessor.trustedChecksums.record=true clean compile
[INFO] Saving 100 checksums to '<scratch>/work/supply-chain/maven-trust/.mvn/checksums/checksums-central-8fac3ebd6edbaca3c794783fa38088af0aa128e7.sha512'
The file has one line per resolved artifact, SHA-512 first and the path relative to the local repository second (100 lines
here, covering the plugins and their dependencies as well as the project’s own dependency). A normal build with a fresh
local repository passes (BUILD SUCCESS). Changing the first two hexadecimal digits of the commons-csv-1.14.1.jar line
makes the build fail, with both values in the message:
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal on project tidepool-engine: Could not resolve dependencies for project org.example.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] dependency: org.apache.commons:commons-csv:jar:1.14.1 (compile)
[ERROR] Could not transfer artifact org.apache.commons:commons-csv:jar:1.14.1 from/to central (https://repo.maven.apache.org/maven2): Checksum validation failed, expected '004fb9c4895532dd9f6cd448dfb4b35bfe9c4a81afc61e5bb13121f5142330ae97447200022b03988967a7cb89d3f48ce76e253238eb9f0bea57c09b352ac27b' (PROVIDED) but is actually 'f64fb9c4895532dd9f6cd448dfb4b35bfe9c4a81afc61e5bb13121f5142330ae97447200022b03988967a7cb89d3f48ce76e253238eb9f0bea57c09b352ac27b'
(PROVIDED) is the Resolver’s name for the checksum strategy that came from your file rather than from the repository. The
Resolver also has a sparse-directory source (one checksum file per artifact, in a tree that mirrors the local repository)
for repositories where one summary file per remote repository would be unwieldy; the Resolver documentation compares the
two sources. Trusted checksums are a Maven-side equivalent of Gradle’s dependency verification (next), with SHA-512 and no
signature support.
Maven: remote repository filtering
When a build lists several repositories, Maven asks them in order, so a repository that should never serve a given artifact can serve it first (dependency confusion), and every miss leaks the coordinates you build to that repository’s operator. Remote repository filtering (RRF) tells the Resolver which repository may serve which artifact. The Resolver 2 documentation, which Maven 3.10 and 4 use, describes two filters: prefixes, a list of path prefixes that the repository itself publishes, discovered automatically; and groupId, a file you write. The prefixes filter is why a Maven 3.10 build against Central prints a line like this on the first resolution:
[INFO] Loaded 24109 auto-discovered prefixes for remote repository central (prefixes-central.txt)
A groupId file for the repository central is groupId-central.txt in the filter directory (set here with
-Daether.remoteRepositoryFilter.groupId.basedir=${session.rootDirectory}/.mvn/rrf/ in .mvn/maven.config). A line is a
group and everything below it; ! negates, = limits to exactly that group, and * sets the default to "accept". This
file blocks the Apache Commons group on Central and allows everything else:
# everything on Central except Apache Commons
*
!org.apache.commons
Building tidepool-engine, which depends on commons-csv, with a fresh local repository on Maven 3.10.0:
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal on project tidepool-engine: Could not resolve dependencies for project org.example.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] dependency: org.apache.commons:commons-csv:jar:1.14.1 (compile)
[ERROR] groupId: G:org.apache.commons NOT allowed from central
In real use the file lists the groups a repository is allowed to serve (for an internal repository, your own group ids),
and Central is left alone: the Resolver documentation warns that limiting what can come from Central is risky because
plugins and their dependencies come from it too. The Resolver can also record the group ids it meets
(-Daether.remoteRepositoryFilter.groupId.record=true) as a starting point for such a file.
On Maven 3.9.16 (Resolver 1.9) the same file behaved differently: with no extra option the build passed (no filtering
happened), and with -Daether.remoteRepositoryFilter.groupId=true it failed on the first plugin, saying that
G:org.apache.maven.plugins was not allowed from central — the * default-accept line did not have the effect it has on 3.10. This
page therefore uses 3.10 for the example; on 3.9.x enable the filter explicitly and list every group allowed.
Gradle: dependency verification
Gradle’s feature is the most complete of the three. A file gradle/verification-metadata.xml lists the checksums (SHA-256
and others) and the PGP keys that are acceptable for each artifact; Gradle checks every dependency, plugin and metadata file
it downloads against it, fails the build on a mismatch, and by default verifies the metadata files (.pom, .module) as
well. The examples below were run on Gradle 9.8.1 only. The modes are --dependency-verification strict (default),
lenient (report but do not fail) and off.
Generating the file
The file is bootstrapped by running a build with --write-verification-metadata. With sha256 alone, every artifact
gets a checksum entry. This is the Kotlin DSL build of the scratch project (a Groovy build file differs only in syntax and
is irrelevant to the verification):
plugins {
`java-library`
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-csv:1.14.1")
}
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-csv:1.14.1'
}
gradle --write-verification-metadata sha256 compileJava
The generated gradle/verification-metadata.xml (header and long XML schema attribute trimmed):
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" ...>
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>false</verify-signatures>
</configuration>
<components>
<component group="commons-codec" name="commons-codec" version="1.19.0">
<artifact name="commons-codec-1.19.0.jar">
<sha256 value="5c3881e4f556855e9c532927ee0c9dfde94cc66760d5805c031a59887070af5f" origin="Generated by Gradle"/>
</artifact>
<artifact name="commons-codec-1.19.0.pom">
<sha256 value="e0f3269fa23de0c83130c5659f5f9514cc5422c0bcdf45f2eae004a78b9fca34" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.commons" name="commons-csv" version="1.14.1">
<artifact name="commons-csv-1.14.1.jar">
<sha256 value="32be0e1e76673092f5d12cb790bd2acb6c2ab04c4ea6efc69ea5ee17911c24fe" origin="Generated by Gradle"/>
</artifact>
<artifact name="commons-csv-1.14.1.pom">
<sha256 value="95df735302c0ab92cec05a2b51e2270758e15f0a64186de65662eb148ebd97fd" origin="Generated by Gradle"/>
</artifact>
</component>
... (commons-io, apache, commons-parent and junit-bom entries omitted)
</components>
</verification-metadata>
(The junit-bom entry arrives through the POM of commons-csv: dependency verification also covers the metadata that
Gradle reads, not only the JARs on the classpath.)
Changing the first two hexadecimal digits of the commons-csv-1.14.1.jar checksum and building again fails, naming the
artifact and the repository it came from:
> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Dependency verification failed for configuration ':compileClasspath'
One artifact failed verification: commons-csv-1.14.1.jar (org.apache.commons:commons-csv:1.14.1) from repository MavenRepo
This can indicate that a dependency has been compromised. Please carefully verify the checksums.
Open this report for more details: file://<scratch>/work/supply-chain/gradle-verify/build/reports/dependency-verification/at-1792142010624/dependency-verification-report.html
Signatures
Checksums alone say "this is the file I saw when I generated the metadata" and change with every upgrade. Signatures say
"this file was signed by a key I trust", which scales better: you trust the key of a project and every release it
signs verifies. Generating with pgp (and sha256 as the fallback for artifacts without a trusted signature) writes
<verify-signatures>true</verify-signatures>, a <trusted-keys> block with the full 40-character fingerprint of every
signing key and the groups it may sign, and per-artifact <pgp> entries for the exceptions. The command was
gradle --write-verification-metadata sha256,pgp compileJava, and the result for the same project was:
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>true</verify-signatures>
<trusted-keys>
<trusted-key id="2DB4F1EF0FA761ECC4EA935C86FDC7E2A11262CB">
<trusting group="commons-codec"/>
<trusting group="commons-io"/>
<trusting group="org.apache.commons"/>
</trusted-key>
<trusted-key id="FF6E2C001948C5F2F38B0CC385911F425EC61B51" group="org.junit" name="junit-bom" version="5.13.1"/>
</trusted-keys>
</configuration>
<components>
<component group="org.apache" name="apache" version="35">
<artifact name="apache-35.pom">
<pgp value="84789D24DF77A32433CE1F079EB80E92EB2135B1"/>
</artifact>
</component>
</components>
Gradle downloaded the signature files and looked up the public keys while generating this. The
file then verifies on a later run with --refresh-dependencies (so everything is downloaded again). Removing the
<trusting group="org.apache.commons"/> line makes the signatures of three artifacts untrusted and the build stops:
> Dependency verification failed for configuration ':compileClasspath'
3 artifacts failed verification:
- commons-csv-1.14.1.jar (org.apache.commons:commons-csv:1.14.1) from repository MavenRepo
- commons-csv-1.14.1.pom (org.apache.commons:commons-csv:1.14.1) from repository MavenRepo
- commons-parent-85.pom (org.apache.commons:commons-parent:85) from repository MavenRepo
If the artifacts are trustworthy, you will need to update the gradle/verification-metadata.xml file. For more on how to do this, please refer to https://docs.gradle.org/9.8.1/userguide/dependency_verification.html#sec:troubleshooting-verification in the Gradle documentation.
(The anchor sec:troubleshooting-verification named in that message does not exist on the 9.8.1 documentation page; the section about failures is sec:dealing-verification-failure.)
Trusting a key is a decision, not a formality: the key id in an error message comes from the signature file itself, so
Gradle cannot tell you the signer is the project you think it is. Check the fingerprint against the project’s published
keys before you add it. To avoid depending on key servers on every machine, export the keys used into a keyring file
(gradle/verification-keyring.keys or .gpg) with --export-keys and commit one of the two formats. The documentation
also covers <ignored-key> for keys that are not available, <trusting> regular expressions, sha256 fallbacks for
artifacts that are not signed, a dry mode for checking an update, and the report that the failure message points to.
Verification is not a replacement for choosing the right repositories: the three 2026 advisories about repositories that Gradle failed to disable (below) all name strict repository content filtering and dependency verification as the mitigation, which is why the two features are described together in Dependency Locking, Verification & Caching and Repositories & Content Filtering.
Source: Gradle: Checksum verification
Source: Gradle: Signature verification
Source: Gradle: Exporting keys
Signatures with Maven and Ant
Maven itself does not verify PGP signatures of dependencies. Central requires signatures on published artifacts (see the
Central requirements and
Publishing to Maven Central), but the
consumer side is up to extensions. A third-party plugin exists for that (the PGPVerify Maven plugin, a separate project that
this series did not run); the first-party answer for Maven is the trusted-checksums mechanism above. Ant and Ivy have no
built-in signature verification either, so for them verify with your own gpg --verify step in the build or in CI.
Credentials
A build that publishes or reads from a private repository needs a secret. The rules are the same for all three tools: keep it out of the repository and out of logs, scope it to the repository that needs it, and prefer a short-lived token supplied by CI to a long-lived password in a file.
Maven: settings.xml
Credentials live in ~/.m2/settings.xml <server> entries matched to a repository by <id>. Three Maven-specific layers
protect them:
-
Password encryption. Maven 3 encrypts passwords with a master password that lives in
~/.m2/settings-security.xml. The Maven guide itself calls this obfuscation-grade (single-hash key derivation, no authenticated encryption): it keeps secrets out of logs and screenshots but does not resist an attacker who can read your files. Creating an encrypted value (the argument is a placeholder; real use lets Maven prompt):mvn --encrypt-master-password 'placeholder-master'{O0dVSUFOQRoHGc8COO+KVkNwfTAqd2KRQwc0mgRxmNtgyB90AdmIp8ohTk9vMtFv}The value differs on every run (Maven salts it), and a
<server>password written as{...}is decrypted at use time. -
Maven 4 (release candidate). Replaces this with
mvnencand pluggable "dispatchers", AES/GCM with a PBKDF2 key, and master-key sources including a file, an environment variable, a system property, the GnuPG agent, Pinentry and the 1Password CLI. Setup ismvnenc init, which is interactive and not run here; the diagnosis command can be run without it and shows what an unconfigured installation looks like:mvnenc -B diag[INFO] Configuration validation of MavenSecDispatcher: INVALID [INFO] Legacy dispatcher present in system [ERROR] No configuration file found on path <home>/.m2/settings-security4.xml [INFO] Configuration validation of LegacyDispatcher: INVALID [ERROR] Legacy configuration not found or does not contains encrypted master password(One more line, a warning that the legacy dispatcher is not operational, is omitted from this output.
mvnenc encryptwithout a configuration ends with "Maven Encryption is not configured, runmvnenc initfirst.") -
Origin-scoped credentials (Maven 3.10). Before 3.10 a
<server>could be used for any repository that happened to share its id. In 3.10 the credentials of a server are bound to the origin (scheme, host and port) of the repositories and mirrors declared with the same id, the id is matched exactly instead of by prefix, and<aliases>lets one set of credentials serve several ids. When the repository is not known at session start (for example one added by a profile activated with-P),<repositoryOrigins>declares the origins explicitly, written asscheme://host[:port]. This was not run here; the settings below follow the 3.10.0 settings reference, with environment variables supplying the secret:<settings> <servers> <server> <id>internal</id> <username>${env.REPO_USER}</username> <password>${env.REPO_TOKEN}</password> <aliases> <alias>internal-snapshots</alias> </aliases> <repositoryOrigins> <repositoryOrigin>https://repo.example.org</repositoryOrigin> </repositoryOrigins> </server> </servers> </settings>
Maven 3.10 added full settings.xml interpolation, which is what makes ${env.REPO_TOKEN} work for all values.
Source: Maven: Password Encryption
Source: Maven 4: Password Encryption
Gradle: credentials as properties
Declare the credentials by type and let Gradle look the values up from properties named after the repository. For a
repository called internal the properties are internalUsername and internalPassword, which come from
~/.gradle/gradle.properties, the command line (-P) or, in CI, environment variables named
ORG_GRADLE_PROJECT_internalUsername and ORG_GRADLE_PROJECT_internalPassword. The build file contains no secret and no
property is read until the repository is used:
repositories {
maven {
name = "internal"
url = uri("https://localhost:8443/repository/releases")
credentials(PasswordCredentials::class)
}
mavenCentral()
}
repositories {
maven {
name = 'internal'
url = uri('https://localhost:8443/repository/releases')
credentials(PasswordCredentials)
}
mavenCentral()
}
Without the properties, both builds stop with a message that names them:
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> The following Gradle properties are missing for 'internal' credentials:
- internalUsername
- internalPassword
With placeholder values supplied through the environment and a local server on port 8443 that answers in plain text (so the
TLS handshake fails, localhost only), the build does not fall through to the second repository. It fails on the first
one, which is the behaviour the security fix in Gradle 9.8.1 guarantees (see the advisories below):
> Could not resolve org.apache.commons:commons-csv:1.14.1.
> Could not get resource 'https://localhost:8443/repository/releases/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom'.
> Could not HEAD 'https://localhost:8443/repository/releases/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.pom'.
> Unsupported or unrecognized SSL message
The older behaviour (continuing with mavenCentral() after such an error) could not be run here, because Gradle versions
before the fix were not installed.
Gradle 9.8.0 added reuse of Maven mirror settings (org.gradle.mirror.maven.settings=true), so a single internal mirror
configuration in settings.xml serves both tools; the credentials-by-property mechanism above is unaffected.
Source: Gradle: Handling credentials
Ant and Ivy
Ant has no credential store: the get task takes username and password attributes, and tasks such as scp take their
own, so pass them as properties from the command line (-Drepo.password=...) or from a properties file outside the
repository, never literals in build.xml. Ivy settings declare a credentials element with host, realm, username and passwd attributes, and the file that
holds them should not be committed either.
Source: Ivy: credentials
SBOMs
A software bill of materials (SBOM) lists what a build contains, so that a later advisory can be matched against it. CycloneDX is the format used by all three ecosystems' tooling below. The three tools approach it differently: Gradle and Maven can discover the dependency graph, Ant only records what you write down (or what Ivy knows).
Ant: CycloneDX Antlib 0.2
The Antlib (released 2026-09-25, on Maven Central as org.apache.ant:ant-cyclonedx) provides a componentbom task and the
types component, license, organization and others; it needs Ant 1.10.x (not guaranteed before 1.10.18) and Java 9
or later at run time, creates CycloneDX 1.7 by default, and can read Ivy files through an ivyModule resolver. For a
project with manual dependency management you list the components yourself. The Antlib and its dependencies (CycloneDX
Core Java, Jackson and others, 13 JARs) were fetched into a lib directory with the Maven dependency plugin from the
ant-cyclonedx coordinates, then used with ant -lib lib:
<project name="tidepool-engine" default="sbom" xmlns:cdx="antlib:org.apache.ant.cyclonedx">
<property name="version" value="1.0.0"/>
<target name="sbom" description="Writes a CycloneDX SBOM for tidepool-engine">
<mkdir dir="${basedir}/build/sbom"/>
<cdx:component id="commons-csv" name="commons-csv" group="org.apache.commons"
version="1.14.1" unknownDependencies="true"/>
<cdx:componentbom bomName="tidepool-engine-bom" format="json"
outputDirectory="${basedir}/build/sbom">
<component name="tidepool-engine" group="org.example.tidepool"
version="${version}" description="Tide height engine">
<dependency componentRef="commons-csv"/>
</component>
<additionalComponent refid="commons-csv"/>
</cdx:componentbom>
</target>
</project>
The outputDirectory must exist (a first attempt without the mkdir failed with "outputDirectory must point to a
directory"), and exactly one nested component is the subject of the SBOM, with further components declared through
additionalComponent. Running it and listing the package URLs:
ant -noinput -lib lib sbom
grep -E '"(bomFormat|specVersion)"' build/sbom/tidepool-engine-bom.json
grep -E '"purl"' build/sbom/tidepool-engine-bom.json | sort -u
[cdx:componentbom] creating CycloneDX SBOM <scratch>/work/supply-chain/ant-sbom/build/sbom/tidepool-engine-bom.json
BUILD SUCCESSFUL
Total time: 0 seconds
"bomFormat" : "CycloneDX",
"specVersion" : "1.7",
"purl" : "pkg:maven/org.apache.ant/ant-cyclonedx@0.2?type=jar",
"purl" : "pkg:maven/org.apache.commons/commons-csv@1.14.1?type=jar",
"purl" : "pkg:maven/org.example.tidepool/tidepool-engine@1.0.0?type=jar"
The first purl is the Antlib itself, recorded in the SBOM’s metadata.tools section. The SBOM contains
commons-csv because the build file says so, not because anything scanned the classpath: with Ant the SBOM is only as
accurate as the list you maintain, or as the Ivy descriptor you point it at.
Source: Apache CycloneDX Antlib
Source: CycloneDX Antlib: componentbom
Maven: CycloneDX Maven plugin
The CycloneDX Maven plugin is a third-party project (2.9.3 on Maven Central on the check date). makeBom can be run
without touching the POM:
mvn org.cyclonedx:cyclonedx-maven-plugin:2.9.3:makeBom
[INFO] --- cyclonedx:2.9.3:makeBom (default-cli) @ tidepool-engine ---
[INFO] CycloneDX: Writing and validating BOM (XML): <scratch>/work/supply-chain/maven-sbom/target/bom.xml
[INFO] attaching as tidepool-engine-1.0.0-SNAPSHOT-cyclonedx.xml
[INFO] CycloneDX: Writing and validating BOM (JSON): <scratch>/work/supply-chain/maven-sbom/target/bom.json
[INFO] attaching as tidepool-engine-1.0.0-SNAPSHOT-cyclonedx.json
[INFO] BUILD SUCCESS
"bomFormat" : "CycloneDX",
"specVersion" : "1.6",
"purl" : "pkg:maven/commons-codec/commons-codec@1.19.0?type=jar",
"purl" : "pkg:maven/commons-io/commons-io@2.20.0?type=jar",
"purl" : "pkg:maven/org.apache.commons/commons-csv@1.14.1?type=jar",
"purl" : "pkg:maven/org.example.tidepool/tidepool-engine@1.0.0-SNAPSHOT?type=jar"
(Two warnings about unknown JSON-schema keywords, printed by the plugin’s validator between the JSON lines, are omitted from this output; they did not affect the result.) Both
transitive dependencies of commons-csv appear, which the Ant SBOM above does not list because nothing told it about
them. To produce the SBOM on every build, bind the goal to a phase in the POM; the attaching as lines show that the
files are attached to the project and therefore installed and deployed with it. A multi-module project uses
makeAggregateBom.
Source: CycloneDX
Gradle: CycloneDX Gradle plugin
The CycloneDX Gradle plugin (plugin id org.cyclonedx.bom, 3.5.1 on the Gradle Plugin Portal on the check date, also third
party) adds a cyclonedxBom task. Setting the project’s group and version avoids the plugin’s warning that it will
otherwise use "unspecified".
plugins {
`java-library`
id("org.cyclonedx.bom") version "3.5.1"
}
group = "org.example.tidepool"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-csv:1.14.1")
}
plugins {
id 'java-library'
id 'org.cyclonedx.bom' version '3.5.1'
}
group = 'org.example.tidepool'
version = '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-csv:1.14.1'
}
gradle cyclonedxBom
grep -E '"(bomFormat|specVersion)"' build/reports/cyclonedx/bom.json
grep -E '"purl"' build/reports/cyclonedx/bom.json | sort -u
> Task :cyclonedxDirectBom
> Task :cyclonedxBom
BUILD SUCCESSFUL in 13s
"bomFormat" : "CycloneDX",
"specVersion" : "1.6",
"purl" : "pkg:maven/commons-codec/commons-codec@1.19.0?type=jar",
"purl" : "pkg:maven/commons-io/commons-io@2.20.0?type=jar",
"purl" : "pkg:maven/org.apache.commons/commons-csv@1.14.1?type=jar",
"purl" : "pkg:maven/org.example.tidepool/tidepool-engine@1.0.0-SNAPSHOT?project_path=%3A",
The Groovy DSL build produced bom.json and bom.xml in the same directory (checked with ls; its contents were not
compared). The resolution is that of the build’s runtime classpath, so the transitive dependencies are listed; whichever
tool creates it, an SBOM generated at build time from the resolved graph is more trustworthy than one maintained by hand.
The security releases of 2026
The table lists the 2026 fixes in the tools themselves that this series targets, and the identifier and affected versions as each vendor states them. Use it to decide whether an upgrade is overdue; the first column names the release that contains the fix, which is always the baseline of this section or later.
| Fixed in | Identifier | What was wrong | What changed |
|---|---|---|---|
Ant 1.10.18 (2026-09-06) |
CVE-2026-78254, rated Moderate |
The |
|
Ant 1.10.19 (not released on 2026-10-16) |
CVE-2026-94114 (in Apache Commons BCEL) |
BCEL, the bytecode library, cached attacker-supplied classes under the name they declared, without validating the requested one. |
The Ant source tree’s change log lists "Updated BCEL dependency to 6.13.0 because of CVE-2026-94114" under 1.10.19, which
had not been released on the check date. The 1.10.18 distribution contains the optional |
Ivy 2.6.0 (July 2026) |
CVE-2026-26032, rated Medium |
The |
Fixed in 2.6.0. Only builds that use the packager resolver are exposed. |
Maven 3.10.0 (2026-09-27) |
No CVE on the Maven security page |
Hardening listed in the release notes (next section). |
See below. |
Gradle 9.8.1 and 8.14.6 (October 2026) |
Three advisories rated High (CVSS v4 7.3 to 8.6): GHSA-mvvg-497x-hmj8, GHSA-xwqc-3h47-hg64 and GHSA-j5m7-59rp-24f5. No CVE number was listed on them when checked. |
The daemon accepted Java-serialized objects from its local TCP channels before authenticating the client (client to daemon) or at all (worker to daemon), which can mean code execution in the daemon JVM or denial of service for an attacker who can reach the loopback port. Separately, a failed SSL connection to a repository did not disable that repository, so resolution could continue to the next one. |
Upgrade to 9.8.1 or 8.14.6. The workarounds the advisories give are to avoid shared multi-user machines for builds, and to use strict repository content filtering and dependency verification for the SSL case. |
Gradle 9.3.0 and 8.14.4 (earlier in 2026) |
CVE-2026-22865 (GHSA-mqwm-5m85-gmcv) and CVE-2026-22816 (GHSA-w78c-w6vf-rw82), both High (8.6) |
The same family: after retries on transient errors, or on an unknown host name, Gradle moved on to the next repository instead of failing, so a build could resolve a dependency from a repository it did not mean to use. |
Fixed in 8.14.4 and 9.3.0; the SSL-failure case of the same behaviour was fixed later, in 9.8.1 and 8.14.6. |
The 9.8.1 release notes call the three advisories "high rated" and note that 8.14.6 contains the same fixes; the advisories give 8.14.6 and 9.8.1 as the patched versions and "earlier than 8.14.6, and 9.0.0 to 9.8.0" as affected.
Source: Ant: Security Reports
Source: Ivy: Security Reports
Source: Gradle 9.8.1 Release Notes
Source: Gradle 8.14.6 Release Notes
Maven 3.10 hardening
The Maven 3.10.0 release notes list these under "Security Hardening":
-
credentials of a settings
<server>are scoped to the origin of the repositories and mirrors with the same id, and the server id is matched exactly (see Credentials above); -
a model resolved from a repository (a dependency, parent or imported BOM) contributes less to the build: restricted interpolation, profile activation limited to platform-derived criteria, recessive repository merging and rejection of
system-scoped dependencies, with themaven.model.dependencyInterpolation.fullproperty as an opt-out; -
repositories supplied by the request or the session keep precedence over the ones declared by a resolved model;
-
artifact and metadata coordinates, relocation coordinates and legacy metadata file name tokens are validated before use.
The same release raises POM validation (duplicate plugins or dependencies, invalid characters in versions and others now
fail the build; -Dmaven.resolver.validation=OFF (upper case; off fails with No enum constant) relaxes the new coordinate validation for a migration), makes reproducible builds the
default (the super POM sets project.build.outputTimestamp) and changes the classpath order to breadth-first
(-Daether.system.dependencyVisitor=preOrder restores the old order). The validation and ordering changes are not security
fixes but matter when you upgrade: see
Maven Security & Reproducible Builds.
Source: Maven 3.10.0 Release Notes
Earlier advisories still worth knowing
| Identifier | Tool and fix | Why it matters today |
|---|---|---|
Maven 3.8.1: |
Explains why an HTTP repository fails with "Blocked mirror" (shown above). The Maven security page notes the issue was a vulnerability for builds that connect directly to remote repositories rather than to a repository manager. |
|
Ant 1.10.11 and 1.9.16: a specially crafted tar or zip-format archive (including JARs) could make a build allocate large amounts of memory and fail. Low. |
Any Ant build that reads archives from outside sources should be on 1.10.11 or later; Ant 1.9 has been end of life since 2024. |
|
CVE-2022-46751 (Ivy 2.5.2), CVE-2022-37865 and CVE-2022-37866 (Ivy 2.5.1) |
Ivy: XML external entity injection when parsing its own configuration, Ivy files or POMs (DTD processing off by default since 2.5.2); archive extraction and cache path traversal. Medium. |
Ivy 2.6.0 contains all three fixes plus the 2026 one; do not use Ivy 2.4 or 2.5.0 builds. |
Source: Maven: Security Reports
A hardening checklist
| Concern | Ant (and Ivy) | Maven | Gradle |
|---|---|---|---|
Verify the tool |
SHA-512 and PGP from the Ant download page. |
SHA-512 and PGP from the Maven download page. |
SHA-256 from the release-checksums page. |
Pin the wrapper |
Not applicable (no wrapper). |
|
|
HTTPS only |
Your own discipline. |
Default HTTP blocker (since 3.8.1). |
|
Pin dependencies |
Own |
Trusted checksums (SHA-512), |
|
Limit where artifacts come from |
Single trusted repository. |
Remote repository filtering (prefixes automatic in 3.10; groupId file). |
Repository content filtering and dependency verification. |
Secrets |
Properties from outside the repository. |
|
|
SBOM |
CycloneDX Antlib 0.2; the Ant distribution ships its own. |
CycloneDX Maven plugin (third party). |
CycloneDX Gradle plugin (third party). |
The practice that matters most is shared by the three columns: commit the files that pin trust (checksum file, verification metadata, wrapper properties), review changes to them as you would review code, and upgrade the build tool itself when a security release lands. For the tool-specific details see CI & IDE Integration for running these checks in pipelines, and Comparing Ant, Maven & Gradle for where security features fit among the other differences.