On December 10, 2021, Log4Shell (CVE-2021-44228) turned a single deserialization bug in Apache Log4j into the most urgent patch cycle the Java ecosystem had seen in years — and a huge share of affected organizations couldn't even find every place the vulnerable code lived. JFrog's research team scanned Maven Central in the aftermath and found that in roughly 65% of cases where log4j-core classes appeared inside an artifact, they weren't present as a clean log4j-core-2.x.jar dependency at all — they'd been repackaged directly as .class files inside a shaded or uber JAR. Of those direct-inclusion cases, about 30% didn't show up in the artifact's declared transitive dependency list either, meaning even a careful mvn dependency:tree walk would report the project as clean. A 2026 academic study analyzing 1,808 popular open-source Java Maven projects found the pattern is far from a Log4Shell-only anomaly: nearly half contained at least one hidden or modified dependency tied to a known CVE, averaging more than eight per affected project. This post explains why shading breaks standard software composition analysis (SCA), and what detection actually has to look like to catch it.
Why does shading defeat standard SCA scanning?
Shading defeats standard SCA scanning because most tools identify a dependency by its metadata — groupId:artifactId:version from pom.xml, a lockfile entry, or a META-INF/maven/*/pom.properties file bundled inside the JAR — not by the actual bytecode present. The Maven Shade Plugin and Gradle Shadow plugin exist specifically to merge a dependency's compiled .class files into your own output artifact, producing a single "fat" or "uber" JAR you can run with java -jar. That merge step routinely strips or overwrites the metadata a scanner relies on: pom.properties files get excluded by default filters, and the shade plugin's relocation feature can rewrite package names (org.apache.logging.log4j becomes com.mycompany.shaded.log4j) specifically to avoid classpath collisions when two dependencies bundle different versions of the same library. The vulnerable bytecode still executes at runtime — the JVM does not care what package a class claims to be in — but the artifact-level fingerprint a scanner searches for is gone.
How big is the blind spot in practice?
The blind spot is large enough that manifest scanning alone leaves real CVEs undetected in a substantial share of production artifacts. JFrog's Log4Shell analysis is the clearest data point: across the Maven Central artifacts it examined, direct .class-file inclusion was almost twice as common as clean-jar inclusion, and roughly a third of those instances were absent from the transitive dependency graph a build tool would report. The 2026 arXiv study "Uncovering Hidden Inclusions of Vulnerable Dependencies in Real-World Java Projects" extended this beyond Log4j specifically, finding hidden or modified vulnerable dependencies in close to 50% of the 1,808 popular Maven projects it analyzed, at an average of eight-plus hidden vulnerable dependencies per affected project. Both findings point to the same mechanism: dependency-tree analysis, which walks declared pom.xml relationships, structurally cannot see a dependency once its build-time identity has been merged away — it isn't a coverage gap that more crawling fixes, it's a category the technique doesn't reach.
What does detection have to look like instead of metadata matching?
Detection has to shift from filename and POM matching to fingerprinting the bytecode itself, because that's the one thing shading can't remove without breaking the code. That means hashing individual .class files or their constant-pool signatures and comparing them against known-vulnerable and known-patched versions of a library, rather than trusting a declared version string. This is exactly the approach purpose-built Log4Shell tools took in December 2021: utilities like log4j-detector and log4shell-finder walked JAR contents at the class level, recursively unpacking nested archives, and matched against known bytecode signatures for JndiManager.class and related classes across vulnerable Log4j 2.x releases — catching instances that filename-based tools like a simple find . -name "log4j-core*.jar" missed entirely. The same technique generalizes: any vulnerability tied to specific bytecode can, in principle, be fingerprinted the same way, independent of what the surrounding JAR or package claims about itself.
Where else does the fat-JAR problem show up?
The fat-JAR problem shows up anywhere a build packages dependencies as embedded class files rather than as separate declared artifacts, which in modern Java deployment is nearly everywhere. Spring Boot's default packaging produces an executable fat JAR with dependency classes nested under BOOT-INF/lib/, itself a JAR-inside-a-JAR structure that a scanner has to recursively unpack rather than treat as an opaque binary. WAR files built for servlet containers embed dependency JARs under WEB-INF/lib/, and container images add another layer — a base image can carry an older, vulnerable JAR baked into a filesystem layer well before your own build ever runs. Each of these is a legitimate, common packaging pattern, not a red flag on its own; the risk is specifically that each one adds a level of nesting or repackaging that filename- and manifest-based scanning has to explicitly account for, and many general-purpose SCA tools were originally built around flat, declared-dependency assumptions from ecosystems like npm and PyPI that don't have an equivalent to JAR shading.
How should security teams close this gap?
Security teams should treat bytecode-level fingerprinting and full unpacking of nested archives as a requirement for Java SCA coverage, not an optional enhancement, given how common shading and fat-JAR packaging are in real deployments. Concretely, that means insisting any Java scanning tool recursively opens nested JARs (including BOOT-INF/lib/, WEB-INF/lib/, and JAR-in-JAR structures) and container image layers rather than stopping at the top-level artifact, and that it can identify a known-vulnerable class by content, not only by the version string in a manifest that shading may have altered or removed. Safeguard's deep dependency scanning already walks resolved dependency graphs across Maven and Gradle projects — including container layers — to depth 100 specifically because real vulnerable components, historically including incidents like SolarWinds SUNBURST, tend to sit several levels deep rather than at the top of the tree; pairing that depth with reachability analysis is what turns a raw hit into a finding worth a sprint instead of a ticket nobody triages. The JFrog and 2026 academic findings both point the same direction: for shaded and uber JARs specifically, depth and reachability only help once the underlying class-level content has actually been examined.