
Offline static checker that inspects packaged Java jars for vulnerable netty-resolver-dns versions and detects whether Spring WebClient actually uses Netty's DNS resolver.
Offline checker for three DNS cache poisoning CVEs in io.netty:netty-resolver-dns —
CVE-2026-45674, CVE-2026-47691 and CVE-2026-45673 — and for the question the
version number cannot answer: is your application actually using that resolver?
If you use Spring WebClient, the answer is very likely yes, even though
netty-resolver-dns is nowhere in your pom.xml and nothing in your code calls it.
java -jar netty-resolver-dns-check-0.1.0.jar path/to/your-app.jar
netty-resolver-dns arrives four levels deep:
spring-boot-starter-webflux
└ spring-boot-starter-reactor-netty
└ reactor-netty-http
└ reactor-netty-core
└ netty-resolver-dns (every link: compile scope)
And it is not just on the classpath. Reactor Netty's HttpClient — the connector Spring Boot
picks for WebClient whenever Reactor Netty is present — resolves host names with Netty's own
DnsAddressResolverGroup, not the JDK resolver:
HttpClientConfig.defaultAddressResolverGroup() → HttpResources.getOrCreateDefaultResolver()TcpResources.getOrCreateDefaultResolver() → NameResolverProvider.newNameResolverGroup(...)NameResolverProvider.newNameResolverGroup → new DnsNameResolverBuilder() → new DnsAddressResolverGroup(builder)(Same in reactor-netty 1.1.x, 1.2.x, 1.3.x and main.) Spring Boot's connector detection order is
reactor > jetty > httpComponents > jdk, so Reactor Netty wins whenever it is there.
We did not stop at reading source. In the controlled experiment below, a default Spring Boot 3.5.14
app making one WebClient request sent one real DNS query through Netty's resolver
(WRITE: UDP ... DefaultDnsQuestion(example.com.)).
The advisories for CVE-2026-45674 and CVE-2026-47691 say it plainly: "Any application using Netty's DNS resolver is impacted."
All three CVEs share the same fix: 4.1.135.Final (4.1 line) / 4.2.15.Final (4.2 line).
Read from each spring-boot-dependencies-<v>.pom (<netty.version>), 2026-09-11:
| Spring Boot | default netty | |
|---|---|---|
| 3.4.0 – 3.4.13 | 4.1.115 – 4.1.130 | affected — no 3.4.x release ships the fix |
| 3.5.0 – 3.5.14 | 4.1.121 – 4.1.132 | affected |
| 3.5.15 + | 4.1.135 | fixed |
| 4.0.0 – 4.0.6 | 4.2.7 – 4.2.12 | affected |
| 4.0.7 + | 4.2.15 + | fixed |
The fixed versions are all on Maven Central. Upgrading works; this tool does not claim otherwise.
A version number is one line of mvn dependency:tree. The harder question is whether the
resolver is really in use. So we built four real Spring Boot 3.5.14 fat jars and one on 3.5.16,
and measured the ground truth — DNS queries actually sent by Netty — against what this tool
infers from the jar alone:
| App | What it does | Netty DNS queries (runtime) | This tool (static) |
|---|---|---|---|
| A | default WebClient.builder() | 1 | in use |
| B | .resolver(DefaultAddressResolverGroup.INSTANCE) | 0 | override found → check manually |
| C | spring.http.reactiveclient.connector=jdk | 0 | override found → check manually |
| D | two WebClients, only one overridden | 1 | override found → check manually |
| E | default, Spring Boot 3.5.16 | 1 (on the fixed 4.1.135) | not affected |
The honest result: the tool reliably says "in use" when there is no override, and it finds override traces in your code and config (3 of 3, no false positive on A). It cannot tell B (everything overridden) from D (one client still on the default) — their bytecode evidence is identical. So an override never makes it say "not affected"; it says "check manually", and gives you the one-minute check below.
--logging.level.io.netty.resolver.dns=DEBUGWRITE: UDP in the log — present: Netty's resolver is in use; absent: that request did not use itDo not judge by "was DnsNameResolver loaded?". App B replaced the resolver and still loaded
that class, because the default resolver group is built eagerly. Only the query lines are proof.
java -jar netty-resolver-dns-check-0.1.0.jar <directory | jar | war>
java -jar netty-resolver-dns-check-0.1.0.jar --version-of 4.1.132.Final
It reads what is actually packaged (fat jar BOOT-INF/lib, war WEB-INF/lib, nested jars),
not pom.xml — the dependency is transitive, so the pom would tell you it is not there.
Presence is decided by the class io/netty/resolver/dns/DnsNameResolver.class, not by version
manifests. Override traces are searched only in your own classes and application*.properties/yml
(library jars reference these classes themselves).
Output is in Chinese; the verdict lines and CVE ids are what scripts should key on.
HttpClient directly (for example a gateway) are not
analysed separately — if Reactor Netty is present, the default path is assumed.GitHub advisory API, read 2026-09-11:
| CVE | GHSA | Severity | Affected → fixed |
|---|---|---|---|
| CVE-2026-45674 | GHSA-676x-f7gg-47vc | high 8.7 | <= 4.1.134.Final → 4.1.135.Final · 4.2.0–4.2.14 → 4.2.15.Final |
| CVE-2026-47691 | GHSA-5pvg-856g-cp85 | high 8.7 | same |
| CVE-2026-45673 | GHSA-xmv7-r254-6q78 | medium 6.8 | same |
The Spring Boot table is in BootTable.java; a unit test re-derives the three statements in
section 2 from it, so the table and the claims cannot drift apart silently.
evidence/ contains the five apps (A–E) and runtime.sh, which counts WRITE: UDP lines per app.
tools/e2e_real_jars.py runs this tool against the same five jars and asserts each verdict.
Building the apps needs Maven 3.6.3 or newer.
| Code | Meaning |
|---|---|
| 0 | none of the three CVEs applies (fixed version) |
| 1 | affected version found — including when override traces were found |
| 2 | cannot judge (no netty-resolver-dns found, unknown version, bad arguments) |
| 4 | some file could not be read — "I could not read it" must never look like a pass |
Apache License 2.0