Validate the Gradle Distribution SHA-256 Checksum

Set distributionSha256Sum in gradle-wrapper.properties to verify the integrity of the downloaded Gradle distribution.

Explanation

Always set the distributionSha256Sum property in your gradle-wrapper.properties file to verify the integrity of the downloaded Gradle distribution. This ensures the gradle-X.X-bin.zip file matches the official SHA-256 checksum published by Gradle, protecting your build from corruption or tampering.

distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionSha256Sum=2b3f4...sha256-here...f4511

This validation step enhances security by preventing the execution of compromised or incomplete Gradle distributions.

The official SHA-256 checksums can be found on the Gradle releases page.

Validate the Gradle Wrapper on every Upgrade

The Gradle Wrapper runs before your build logic and can deeply affect your build. You should treat any change to the Wrapper as security-sensitive.

Validate the Wrapper JAR and distribution settings every time you upgrade Gradle.

Explanation

When you update Gradle, two Wrapper files typically change:

  • gradle/wrapper/gradle-wrapper.jar

  • gradle/wrapper/gradle-wrapper.properties (distributionUrl and optionally distributionSha256Sum)

You should verify that:

  • The Wrapper JAR is the official binary published by Gradle (not tampered with).

  • The Wrapper JAR checksum matches one of the values published at gradle.org/release-checksums.

  • The distribution URL points to the expected Gradle release.

  • The distribution checksum (if configured via distributionSha256Sum) matches the release you intend to use (also listed on gradle.org/release-checksums).

Running an unverified Wrapper risks executing untrusted code before any of your build’s safeguards run.

Do this

If you use the setup-gradle action (version v4 or newer) for GitHub Actions, Wrapper validation will be performed automatically. The action validates the checksum of every gradle-wrapper.jar in your repository and fails the build if it finds any unknown Wrapper JAR.

If you use a different GitHub Actions setup, you can use the dedicated Gradle Wrapper validation action instead.

Do not Run ./gradlew on Untrusted Projects

When working with an untrusted project, you should exercise caution before running ./gradlew.

Explanation

The gradlew (Gradle Wrapper) script is an executable that downloads and runs a specific version of Gradle. It should be generated by running the Wrapper task with a trusted Gradle installation, but there is no guarantee that a project’s authors have done this. The script or gradle-wrapper.jar could have been modified. The only way to verify this is through checksum verification.

Running an unverified wrapper is similar to running any other untrusted script from the internet. Build scripts (build.gradle, settings.gradle, etc.) can also execute arbitrary code during the configuration phase.

An unreviewed wrapper or build script could potentially access or modify files on your system, install malicious software, or compromise your environment (e.g., stealing environment variables like GITHUB_TOKEN or AWS_SECRET_KEY).

Opening an untrusted project in an IDE such as IntelliJ IDEA or Android Studio will typically trigger a Gradle sync automatically, which executes build logic. Review the project’s wrapper and build scripts before opening it in your IDE.

Do this

1. Review the Build Scripts
  • Inspect Build Files: Look at build.gradle(.kts), settings.gradle(.kts), and gradle.properties for suspicious dependencies, plugins, custom exec tasks, or obfuscated code.

2. Verify the Gradle Wrapper Files
  • Checksum Verification: Use checksum validation to ensure gradle-wrapper.jar hasn’t been tampered with.

  • Check the URL: Open gradle/wrapper/gradle-wrapper.properties and ensure the distributionUrl points strictly to the official site: https://services.gradle.org/.

  • Regenerate the Wrapper: If you have a trusted Gradle installation, delete the project’s gradlew, gradlew.bat, and gradle/wrapper/gradle-wrapper.jar, then run gradle wrapper (not ./gradlew). Compare the regenerated files against the originals, any differences indicate the project’s wrapper may have been modified.

3. Work in an Isolated Environment

You can optionally run the project in a Docker container or a Virtual Machine (VM). This ensures that even if a malicious script runs, it cannot access your files.

Build Output Should Be Byte-for-Byte Reproducible

A build is reproducible when the same source code produces byte-for-byte identical output files, on any machine, at any time.

Explanation

Reproducibility matters far beyond the publishing flow. If binaries remain identical, then downstream consumers can verify that they match the source. Build systems typically verify artifact checksums; non-determinism in artifact bytes defeats caches and deduplication logic without providing any benefit.

With this strong form of reproducibility, the question "Did my change actually change anything?" is easy to answer: compare the output hashes. Gradle’s own up-to-date checks and remote build cache also rely on stable inputs producing stable outputs.

Some common sources of non-determinism in builds are file timestamps inside archives, the order of entries inside archives, and the SDK used to compile classes. For JVM builds, keeping wall-clock timestamps on .class files inside a .jar, or letting archive entries follow the filesystem’s directory-iteration order, means that two clean builds of the same source produce slightly different artifacts with different checksums even though the output is functionally identical.

Since Gradle 9.0.0, archive artifacts are set to be reproducible by default.

This is list of examples is not exhaustive. There are other ways to introduce non-reproducibility in your project. One good test to confirm reproducibility is to try re-building an older version on a different machine, and comparing the output to the published binary.

Example

Don’t Do This

build.gradle.kts
plugins {
    `java-library` (1)
}

tasks.named<Jar>("jar") {
    isPreserveFileTimestamps = true   (2)
    isReproducibleFileOrder = false   (3)
}
build.gradle
plugins {
    id 'java-library' (1)
}

tasks.named('jar', Jar) {
    preserveFileTimestamps = true   (2)
    reproducibleFileOrder = false   (3)
}
1 No java { toolchain { …​ } } block is declared. Gradle has no default toolchain, so the JDK used for compilation falls back to whichever JAVA_HOME is active. Different machines (and even different JDK patch releases on the same machine) emit different bytecode.
2 Sets preserveFileTimestamps to true, overriding the Gradle 9.0+ default of false. Every entry inside the jar is stamped with the build’s wall-clock time, so the same source produces a different jar on every build.
3 Sets reproducibleFileOrder to false, overriding the Gradle 9.0+ default of true. Archive entries are ordered by the filesystem’s directory-iteration order, which varies by OS and filesystem.

Do This Instead

build.gradle.kts
plugins {
    `java-library`
}

java {
    toolchain {
        // Choose your project's required version
        languageVersion = JavaLanguageVersion.of(21) (1)
    }
}
(2)
build.gradle
plugins {
    id 'java-library'
}

java {
    toolchain {
        // Choose your project's required version
        languageVersion = JavaLanguageVersion.of(21) (1)
    }
}
(2)
1 Pin the JDK used to compile so the build is decoupled from each developer’s local JAVA_HOME and from whichever JDK happens to be on the CI image; Gradle auto-provisions a matching JDK if one isn’t found.
2 Leave archive defaults alone — Gradle 9.0+ sets preserveFileTimestamps = false and reproducibleFileOrder = true on every AbstractArchiveTask (Jar, War, Ear, Zip, Tar). The performance cost of these defaults is negligible: reproducibleFileOrder adds a sort over directory listings that’s dominated by I/O and compression work, and dropping timestamps actually makes archives marginally smaller and avoids unnecessary downstream cache invalidation. If you maintain a build that still supports an older Gradle version, set both flags explicitly with tasks.withType<AbstractArchiveTask>().configureEach { …​ }.