SAST scanner · reads manifests only

Java Dependency Scanner

Log4Shell shipped inside thousands of builds that never called Log4j directly. This scanner reads pom.xml and build.gradle as plain text, pulls out each group:artifact and its version, and checks it against a table of twelve known-bad coordinates. No build, no network, no API key.

SASTCritical–MediumDependency Vulnerability12 coordinatesconfidence 0.85

Reading pom.xml

The actor is the scanner alone. It walks every file in the repository snapshot, and any file named pom.xml (at any depth) goes to a Maven routine. The routine never resolves a parent POM or a property; it sees only the text in front of it.

pom.xml three <dependency> blocks groupId org.apache.logging.log4jartifactId log4j-coreversion 2.14.1 groupId com.google.guavaartifactId guavaversion ${guava.version} groupId org.springframeworkartifactId spring-webmvc(no version tag: managed by a BOM) example file; line numbers are illustrative One regex per block <dependency> <groupId>(…)</groupId> <artifactId>(…)</artifactId> (<version>(…)</version>)? line = newlines before match + 1 guard before any lookup: not version or version.startswith("$") then skip, silently version must follow artifactId directly, or it is not captured Coordinates to look up nothing extracted yet log4j-core @ 2.14.1 extracted, line 14 kept: literal version goes to the table lookup guava @ ${guava.version} extracted, line 22 skipped: property reference spring-webmvc @ (none) extracted, line 30 skipped: no version tag

1A pom.xml declares libraries in <dependency> blocks. Three typical ones: a literal version, a version held in a property, and a version left to a BOM.

2One regex finds every block that has groupId, then artifactId, then an optional <version> right after. The coordinate is group:artifact, and the line number is the count of newlines before the match.

3Guard: an empty version, or one starting with $, is skipped. The scanner cannot resolve ${guava.version} or a BOM-managed version, so it reports nothing rather than guess. A vulnerable Guava hidden behind a property is therefore invisible to this scanner.

4Only the log4j block, with a literal 2.14.1, continues to the table lookup.

Lookup and version compare

The table is a fixed dictionary of 12 group:artifact keys. A coordinate that is not a key is never reported, at any version.

Table entry exact key, group:artifact org.apache.logging.log4j: log4j-core range <2.17.1 CRITICAL CVE-2021-44228 (Log4Shell) remote code execution via log message Compare three numbers installed "2.14.1" → [2, 14, 1] limit "2.17.1" → [2, 17, 1] first three parts, padded with 0 [2,14,1] < [2,17,1] true 2 = 2, then 14 < 17: decided only the < operator is supported Findings none yet CRITICAL · 0.85 Vulnerable dependency: …:log4j-core:2.14.1 pom.xml · line 14 fix: upgrade to the latest version snippet: group:artifact:version Same rule, other inputs for log4j-core (limit <2.17.1) 2.17.0[2,17,0] < [2,17,1]vulnerable, one finding 2.21.0[2,21,0] not < [2,17,1]clean: this is the fix 2.14.1-SNAPSHOTint("1-SNAPSHOT") failstreated as not vulnerable, silent 2.14.+int("+") failstreated as not vulnerable, silent

1The surviving coordinate is a dictionary lookup. Log4j-core has one entry: vulnerable below 2.17.1, severity Critical, the Log4Shell CVE.

2Installed version and limit are each split on ., the first three parts are read as integers, and short versions are padded with zeros.

3The rule is installed < limit as a list compare: [2,14,1] < [2,17,1]. The middle numbers differ and 14 is less than 17, so the library is vulnerable. Only the < range form exists in the table.

4A match becomes a Critical finding at confidence 0.85, with the manifest path, the line, and group:artifact:version as the snippet. The fix text is generic: upgrade to the latest version.

5Edge cases. 2.17.0 is still below 2.17.1. 2.21.0 passes, which is the fix. A version with a suffix such as -SNAPSHOT, or a wildcard like 2.14.+, cannot be parsed to integers, and the scanner treats that as not vulnerable, not as an error.

When Struts gives two findings

Most coordinates have one entry. org.apache.struts:struts2-core has two Critical ranges, and each range that matches yields its own finding for the same line.

entry 1: <2.5.33 CRITICAL CVE-2023-50164, path traversal leading to RCE entry 2: <2.3.37 CRITICAL CVE-2017-5638, RCE via Content-Type header (Equifax) 2.3.37 2.5.33 both entries match entry 1 only no entry matches 2.3.30 2.4.0 2.5.33 2.3.30: two findings <2.5.33 yes<2.3.37 yes same file, line and snippet;descriptions name each CVE 2.4.0: one finding <2.5.33 yes<2.3.37 no CVE-2023-50164 only 2.5.33: clean <2.5.33 no<2.3.37 no equal to the limit is not less

1Two entries share one coordinate. The scanner tests every entry for the coordinate; it does not stop at the first match.

22.3.30 is below both limits, so it gets two findings, one per CVE, on the same line.

32.4.0 is below 2.5.33 but not below 2.3.37, so one finding.

42.5.33 equals the first limit. The compare is strict less-than, so nothing is reported.

Gradle declarations

build.gradle and build.gradle.kts go through one regex over the whole file. The table and the compare are the same as for Maven.

build.gradle / build.gradle.kts implementation 'org.apache.logging.log4j:log4j-core:2.14.1' match: 2.14.1 < 2.17.1, CRITICAL implementation("org.apache.commons:commons-text:1.9") match (.kts form): [1,9,0] < [1,10,0], CRITICAL api "com.google.guava:guava:$guavaVersion" version "$guavaVersion" is not a number: silent annotationProcessor 'org.projectlombok:lombok:1.18.30' keyword not in the list: never matched The Gradle regex keyword, then ( ' or ", then three parts implementation | api | compile runtimeOnly | testImplementation group : artifact : version version = everything up to a quote, a ) or whitespace Unlike pom.xml there is no $ guard. A variable version reaches the compare and fails the integer parse instead. testImplementation is scanned too.

1One regex runs over the file text. It needs one of five configuration keywords, an opening quote or parenthesis, and a group:artifact:version string.

2The Groovy single-quoted form matches. Log4j-core 2.14.1 is below 2.17.1, so Critical.

3The Kotlin DSL form implementation("…") matches the same regex. Commons Text 1.9 is below 1.10.0 (Text4Shell), so Critical.

4Two quiet cases. A variable version is captured as text, fails the integer parse and is dropped. A configuration such as annotationProcessor is not in the keyword list, so the line is never seen.

Why it matters

Why it matters

Java dependency vulnerabilities have caused some of the largest breaches in history, because one library sits under thousands of applications. Log4Shell (2021), CVE-2021-44228, allowed remote code execution through a log message. Apache Struts (2017), CVE-2017-5638, caused the Equifax breach. Text4Shell (2022), CVE-2022-42889 in Commons Text, allowed remote code execution through string interpolation.

Real-world breach

Equifax (2017). 147 million records were stolen because of an unpatched Apache Struts dependency. The patch had been available for two months. Equifax paid $700 million in settlements. Log4Shell (2021) affected hundreds of thousands of applications, including Apple, Amazon, Cloudflare and Steam.

Vulnerable vs fixed

<!-- Maven: Log4Shell-era version -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>

// Gradle: same coordinate
implementation 'org.apache.logging.log4j:log4j-core:2.14.1'

Limits and what it stays quiet about

CoordinateLimitSeverityIssue
org.apache.logging.log4j:log4j-core<2.17.1CriticalCVE-2021-44228 (Log4Shell)
org.apache.struts:struts2-core<2.5.33 and <2.3.37CriticalCVE-2023-50164; CVE-2017-5638
org.apache.commons:commons-text<1.10.0CriticalCVE-2022-42889 (Text4Shell)
org.springframework:spring-webmvc<5.3.28HighCVE-2023-34036, DoS
org.springframework.boot:spring-boot-starter<3.1.5HighCVE-2023-34055, DoS via HTTP/2
org.springframework.security:spring-security-core<5.8.9HighCVE-2023-34042, authorization bypass
com.fasterxml.jackson.core:jackson-databind<2.15.3HighCVE-2023-35116, DoS
io.jsonwebtoken:jjwt<0.11.5HighAlgorithm confusion
org.apache.tomcat.embed:tomcat-embed-core<10.1.16HighCVE-2023-46589, request smuggling
mysql:mysql-connector-java<8.0.33HighCVE-2023-21971, unauthorized access
commons-io:commons-io<2.14.0MediumCVE-2024-47554, path traversal
com.google.guava:guava<32.0.0MediumCVE-2023-2976, temp directory
What it deliberately stays quiet about

The table is hand-maintained and holds one or two entries per library, so a library or a CVE not in it is never reported; the OSV scanner covers Maven coordinates with a live database. Property versions in pom.xml (${…}) and BOM-managed versions with no <version> tag are skipped. A version that does not parse as three integers, such as a -SNAPSHOT suffix, a wildcard or a Maven range, counts as not vulnerable. Gradle version catalogs and unlisted configurations such as annotationProcessor are not read.