Safeguard
Security Guides

Gradle Dependency Security: Locking, Verification, and Version Catalogs

Secure Gradle builds in 2026 with dependency locking, cryptographic dependency verification, version catalogs, and reachability-aware CVE scanning.

Marcus Chen
AppSec Engineer
5 min read

Gradle gives you enormous flexibility over how dependencies resolve — dynamic versions, resolution strategies, custom repositories, plugin-applied dependencies. That flexibility is a double-edged sword. A 1.+ version range or a latest.release declaration means the exact bytes in your build can change between two runs with no code change on your side, and a compromised or typosquatted package in one of your configured repositories can slip in unnoticed. The good news is that Gradle ships strong, first-party security controls that most teams simply haven't turned on. This guide walks through the four that matter most.

What makes a Gradle build reproducible and safe?

A safe Gradle build is one where the set of artifacts and their contents are fixed and verified — you get the same, cryptographically confirmed dependencies every time, and any drift fails the build. Gradle offers three native mechanisms to achieve this: version catalogs for centralized declaration, dependency locking for pinned resolution, and dependency verification for cryptographic integrity. Used together, they close the "the build changed but the code didn't" gap that dynamic versions create.

Centralize declarations with a version catalog

A version catalog (gradle/libs.versions.toml) puts every dependency version in one auditable file instead of scattering them across build scripts. One place to review, one place to bump:

[versions]
jackson = "2.17.2"
spring-boot = "3.3.5"

[libraries]
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }

Referenced in a build script as implementation(libs.jackson.databind), this makes version audits and upgrades a single-file operation and eliminates the copy-paste drift where one module quietly runs an old, vulnerable version.

Pin resolution with dependency locking

Version catalogs declare intent; dependency locking records the exact transitive result. Enable locking so every resolved version — direct and transitive — is written to a lockfile and enforced on subsequent builds:

dependencyLocking {
    lockAllConfigurations()
}

Generate or update the lockfile deliberately with ./gradlew dependencies --write-locks. After that, any change to a transitive version fails the build until someone reviews and regenerates the lock — turning a silent supply-chain shift into a reviewable diff. Pair this with banning dynamic versions so ranges can't reintroduce nondeterminism:

configurations.all {
    resolutionStrategy {
        failOnDynamicVersions()
        failOnChangingVersions()
    }
}

Verify integrity with dependency verification

Locking pins which version you get; verification confirms the bytes are authentic. Gradle's dependency verification maintains a gradle/verification-metadata.xml file of checksums and, ideally, PGP signatures for every artifact. Bootstrap it with:

./gradlew --write-verification-metadata sha256,pgp help

Once enabled, Gradle refuses to use any artifact whose checksum or signature doesn't match the recorded trust metadata. This is your strongest defense against a compromised mirror or a tampered artifact: even if an attacker swaps a JAR in a repository, the hash won't match and the build stops. Commit the verification metadata to source control and review changes to it as carefully as you review code.

Scan for known vulnerabilities

Reproducibility and integrity don't tell you whether a pinned, verified dependency is itself vulnerable — a perfectly authentic artifact can still be a version with a critical CVE. You need a scanner that reads the resolved graph. Gradle exposes it directly:

./gradlew dependencies --configuration runtimeClasspath

Feed that resolved graph into a vulnerability scanner and gate the build on findings. As with Maven, a pure CVSS threshold is noisy because it can't distinguish a reachable, exploitable dependency from one whose vulnerable code you never call.

Watch the plugins, not just the libraries

One risk unique to Gradle is that build logic itself is code. Plugins run with full access to your build environment — they can read credentials, alter the resolved dependency set, and execute arbitrary tasks. A compromised or malicious plugin is a supply-chain attack that never touches your implementation dependencies at all. Treat plugins with the same rigor: pin plugin versions in the plugins {} block rather than using dynamic versions, resolve them from trusted repositories only, and include them in your dependency verification metadata. Restrict pluginManagement repositories to the Gradle Plugin Portal and your curated internal mirror, and review any change to the plugin set as carefully as a change to application code:

pluginManagement {
    repositories {
        gradlePluginPortal()
        maven { url = uri("https://nexus.acme.internal/repo") }
    }
}

Gradle security checklist

  • Centralize versions in libs.versions.toml; reference via libs.*
  • Enable dependencyLocking and commit lockfiles; regenerate deliberately
  • failOnDynamicVersions() and failOnChangingVersions() in resolution strategy
  • Enable dependency verification (sha256 + pgp); commit verification-metadata.xml
  • Restrict repositories to a curated, trusted set (guard against dependency confusion)
  • Scan the resolved runtime classpath for CVEs on every build

How Safeguard helps

Gradle's native controls make your build reproducible and tamper-evident, but they can't tell you which pinned dependency is dangerous or whether it matters to your code. Safeguard fills that half. Its software composition analysis reads Gradle's fully resolved classpath — every transitive artifact, not just what your catalog declares — and matches it against known CVEs, while the reachability engine flags only the vulnerabilities actually reachable from your application so build gates stay meaningful instead of getting muted for noise. When a fix exists, auto-fix pull requests update your version catalog and regenerate the affected lock entries automatically, and the Safeguard CLI plugs into ./gradlew runs and CI so the scan happens on every build. If you're comparing scanners for a Gradle-heavy stack, the platform comparison hub breaks down how reachability differs from manifest-only tools.

Create a free account at app.safeguard.sh/register, and see the Gradle integration docs at docs.safeguard.sh.

Never miss an update

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