Dependency Security

Java Maven and Gradle Dependency Security

How to secure your Java dependency chain across Maven and Gradle builds, from signature verification to repository management.

Shadab Khan
Application Security Architect
4 min read

Java applications rely heavily on third-party libraries pulled from Maven Central and other repositories. The Log4Shell vulnerability in December 2021 was a painful reminder of how deep and impactful a single compromised dependency can be. This guide covers the practical steps for securing your Java dependency chain in both Maven and Gradle projects.

The Java Dependency Problem

A typical enterprise Java application has hundreds of dependencies. Spring Boot starters alone pull in dozens of transitive libraries. Maven and Gradle resolve these trees automatically, which is convenient but opaque. Most developers have no idea what is actually in their dependency tree.

The risks are familiar:

  • Known vulnerabilities in transitive dependencies. Your direct dependencies are probably fine. Their dependencies' dependencies are where the risk hides.
  • Dependency confusion. If you use internal Maven repositories alongside public ones, the resolution order matters.
  • Compromised artifacts. Maven Central requires PGP signatures, but verification is not enforced by default in either Maven or Gradle.
  • Abandoned libraries. Java's enterprise ecosystem is littered with libraries that haven't seen a release in years but still have known CVEs.

Maven: Securing Your Build

Enforcing Dependency Versions

The Maven Enforcer Plugin prevents unexpected dependency changes:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.4.1</version>
  <executions>
    <execution>
      <id>enforce</id>
      <goals>
        <goal>enforce</goal>
      </goals>
      <configuration>
        <rules>
          <requireMavenVersion>
            <version>3.8.0</version>
          </requireMavenVersion>
          <dependencyConvergence/>
          <banDuplicateClasses/>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

The dependencyConvergence rule fails the build if different versions of the same dependency appear in the tree. This catches a class of subtle bugs and ensures you know exactly which version is being used.

Dependency Management Section

Use <dependencyManagement> to pin transitive dependency versions explicitly:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.21.1</version>
    </dependency>
  </dependencies>
</dependencyManagement>

This overrides whatever version a transitive dependency might request.

Repository Configuration

Lock down which repositories Maven can pull from. In your settings.xml:

<mirrors>
  <mirror>
    <id>company-repo</id>
    <mirrorOf>*</mirrorOf>
    <url>https://nexus.yourcompany.com/repository/maven-public/</url>
  </mirror>
</mirrors>

The mirrorOf>*</mirrorOf> ensures all artifact requests go through your corporate proxy repository. This gives you a single point of control for blocking malicious packages and caching approved artifacts.

PGP Signature Verification

Maven 4.x will enforce PGP signature verification by default. For Maven 3.x, you can use the pgpverify-maven-plugin:

<plugin>
  <groupId>org.simplify4u.plugins</groupId>
  <artifactId>pgpverify-maven-plugin</artifactId>
  <version>1.17.0</version>
  <executions>
    <execution>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>

This verifies that every artifact in your build was signed by its expected maintainer.

Gradle: Securing Your Build

Dependency Verification

Gradle 6.2+ supports built-in dependency verification. Generate a verification metadata file:

gradle --write-verification-metadata sha256,pgp

This creates gradle/verification-metadata.xml containing checksums and PGP key IDs for every dependency:

<components>
  <component group="com.google.guava" name="guava" version="32.1.3-jre">
    <artifact name="guava-32.1.3-jre.jar">
      <sha256 value="abc123..." origin="Generated by Gradle"/>
      <pgp value="AABBCCDD"/>
    </artifact>
  </component>
</components>

Commit this file. Gradle will verify every dependency on subsequent builds and fail if anything changes.

Dependency Locking

Gradle dependency locking pins resolved versions across builds:

dependencyLocking {
    lockAllConfigurations()
}

Generate lock files:

gradle dependencies --write-locks

This creates per-configuration lock files that ensure reproducible builds.

Repository Filtering

Restrict which repositories can serve which dependencies:

repositories {
    maven {
        url = "https://nexus.yourcompany.com/repository/maven-releases/"
        content {
            includeGroupByRegex("com\\.yourcompany\\..*")
        }
    }
    mavenCentral() {
        content {
            excludeGroupByRegex("com\\.yourcompany\\..*")
        }
    }
}

This prevents dependency confusion by ensuring internal packages can only come from your private repository.

Vulnerability Scanning

OWASP Dependency-Check

The OWASP Dependency-Check plugin scans your dependency tree against the NVD:

Maven:

<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>9.0.6</version>
  <configuration>
    <failBuildOnCVSS>7</failBuildOnCVSS>
  </configuration>
</plugin>

Gradle:

plugins {
    id 'org.owasp.dependencycheck' version '9.0.6'
}

dependencyCheck {
    failBuildOnCVSS = 7.0f
}

Viewing Your Dependency Tree

Before you can secure your dependencies, you need to see them:

# Maven
mvn dependency:tree

# Gradle
gradle dependencies

Pipe the output through grep to find specific libraries. Pay special attention to transitive dependencies you did not explicitly choose.

SBOM Generation

Generate CycloneDX SBOMs as part of your build:

Maven:

<plugin>
  <groupId>org.cyclonedx</groupId>
  <artifactId>cyclonedx-maven-plugin</artifactId>
  <version>2.7.10</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>makeAggregateBom</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Gradle:

plugins {
    id 'org.cyclonedx.bom' version '1.7.4'
}

Include SBOM generation in your CI pipeline and publish the artifacts alongside your JARs.

How Safeguard.sh Helps

Safeguard.sh integrates with your Java build pipelines to provide continuous dependency monitoring. It consumes the CycloneDX SBOMs generated by your Maven and Gradle builds, tracks every library version across your services, and alerts you when new CVEs are published that affect your stack. When the next Log4Shell hits, Safeguard.sh tells you within minutes which of your applications are affected and which specific versions need upgrading. It replaces the manual triage process with automated, prioritized remediation guidance.

Never miss an update

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