Safeguard
Engineering

Java Supply Chain Security Beyond Log4Shell

Log4Shell was the fire drill. The structural problems — unverified Maven resolution, invisible shaded jars, sprawling transitive graphs — are still there. Here's what to actually fix.

Daniel Osei
Security Analyst
6 min read

Java supply chain security did not get fixed by patching Log4Shell; the ecosystem's structural weaknesses — unverified artifact resolution, transitive graphs that average dozens of dependencies per direct one, and shaded jars that hide components from scanners — all survived CVE-2021-44228 intact. The proof arrived on schedule: Spring4Shell (CVE-2022-22965) four months later, Text4Shell (CVE-2022-42889) that autumn, and a steady drip of compromised or malicious artifacts since. If your entire response to Log4Shell was "we upgraded log4j-core to 2.17.1," this post is the rest of the work.

The transitive graph is the attack surface

A typical Spring Boot service declares maybe 15 direct dependencies and resolves 150+ jars. You didn't choose most of them, and neither did anyone on your team. Log4Shell hurt precisely because log4j-core arrived transitively through starters and logging facades — teams spent the first 48 hours just figuring out whether they were affected. We covered how to detect Log4Shell across a portfolio at the time; the durable lesson is that you need the inventory before the CVE drops, not during the incident call.

Start with visibility you already have:

mvn dependency:tree -Dverbose
./gradlew dependencies --configuration runtimeClasspath

Then pin the graph. Maven's dependencyManagement section and Gradle's version catalogs (gradle/libs.versions.toml) give you one authoritative place where versions are decided instead of letting "nearest wins" mediation pick silently. Add the Maven Enforcer plugin with requireUpperBoundDeps and banDuplicatePomDependencyVersions so conflicting transitive versions fail the build instead of resolving to whichever one Maven encountered first.

Verify what you resolve: Gradle got this right

Maven Central artifacts are PGP-signed, but out of the box neither Maven nor Gradle verifies signatures or checksums during resolution. Gradle fixed this properly with dependency verification:

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

That generates gradle/verification-metadata.xml containing a checksum (and optionally trusted PGP keys) for every resolved artifact — including plugins, which people forget are dependencies too. From then on, any artifact that doesn't match fails the build. The maintenance cost is real: dependency bumps require regenerating metadata, and PGP mode involves curating trusted keys. Teams that adopt checksum-only mode keep maybe 95% of the value at 20% of the friction.

On the Maven side, run resolution through an internal repository manager (Nexus or Artifactory) that proxies Maven Central, and turn off any remaining http:// repository URLs — CVE-2021-26291 was exactly this, Maven resolving over insecure HTTP and opening the door to MITM code injection. Any pom.xml in your dependency tree can declare its own repositories; a repository manager with an allowlist neutralizes that.

Shaded and fat jars: where scanners go blind

The uber-jar is Java's most effective SBOM destroyer. When maven-shade-plugin relocates com.fasterxml.jackson into com.example.shaded.jackson, filename-based and even path-based scanners lose it. The vulnerable code is still there; your inventory says it isn't. Log4Shell hunts in 2021 repeatedly missed embedded copies inside vendor fat jars for exactly this reason.

Countermeasures, in order of preference:

  1. Generate SBOMs at build time, before shading, with cyclonedx-maven-plugin or cyclonedx-gradle-plugin — the build knows the truth even if the artifact obscures it.
  2. Scan artifacts with a tool that fingerprints class content and bytecode, not just pom.properties entries.
  3. For third-party fat jars you can't rebuild, demand an SBOM from the vendor or treat the jar as unaudited code, because it is.

Build-time SBOM generation is one plugin block and roughly zero ongoing cost. There is no good excuse for skipping it in 2025.

Malicious packages arrived in Java too

Java skipped the worst of the typosquatting wave for years, mostly because Maven Central's publishing bar (domain-verified groupIds, signed releases, staging review) is higher than npm's. It didn't skip it forever. Researchers have logged hundreds of malicious artifacts across Maven Central and third-party repositories since 2022 — fake Selenium helpers, snooping SDK forks, dependency-confusion probes against internal groupIds published to public repos.

The dependency-confusion variant deserves specific attention: if your internal artifacts use com.yourcompany.* groupIds, register and verify that namespace on Maven Central even if you never publish there. Verification is a DNS TXT record and blocks anyone else from squatting your groupId. It's the cheapest supply chain control in this entire post.

And route builds through your repository manager exclusively, with public repos proxied and internal groupIds pinned to the internal repo — most repository managers call this "routing rules." That closes the "public artifact shadows internal artifact" resolution race that made dependency confusion famous.

Continuous scanning, because point-in-time audits rot

Everything above is preventive structure. You still need detection: new CVEs land against your existing graph daily, and OWASP Dependency-Check or a commercial SCA scanner on every PR — plus scheduled scans against deployed artifacts — is what turns "a CVE was published Tuesday" into a ticket Wednesday rather than a breach report next quarter. Safeguard's Java analysis pairs manifest data with binary fingerprinting for the shaded-jar case; whichever tool you pick, test it against a deliberately shaded jar before trusting it. Vendors differ wildly here, and the difference doesn't show up in a demo against a clean pom.xml.

A closing benchmark: when the next log4j-class event hits, the teams that answer "are we affected, where, and is it reachable" in an hour all have the same three things — a pinned graph, build-time SBOMs, and PR-gated scanning. None of those were bought during the incident.

Frequently asked questions

Is Maven Central safe to use directly?

It's the best-run public Java repository — domain-verified namespaces, mandatory signing, immutable releases — but "safe" oversells it. Malicious and vulnerable artifacts still get published; proxy it through a repository manager and scan what you resolve.

Do I need Gradle dependency verification if I already use a repository manager?

They cover different failure modes. The repository manager controls where artifacts come from; verification metadata proves what you resolved matches what you expected, catching tampering anywhere between publisher and build, including a compromised repository manager.

How do I find vulnerable libraries inside shaded jars?

Generate SBOMs at build time before shading, and scan third-party jars with tools that identify components by class fingerprints or bytecode rather than manifest filenames. If a vendor can't give you an SBOM for their fat jar, assume it contains something old.

What single control gives the most value for the least effort?

Namespace verification of your internal groupIds on Maven Central (blocks dependency confusion, costs one DNS record), followed closely by adding the CycloneDX plugin to every build. Both are under an hour of work.

Never miss an update

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