
Offline Java tool that scans application jars to determine exposure to 14 Netty codec-http CVEs, identifying the exact patched version (4.1.137.Final/4.2.17.Final) despite inconsistent advisory boundaries.
# netty-http-check
**Offline checker for 23 `io.netty:netty-codec-http` advisories (2025–2026)** — the 14 CVEs in the
GitHub Advisory Database, plus **9 advisories that Netty published on 2026-09-10 only in its own
repository, which Dependabot cannot see.**
It tells you which ones you are actually exposed to, and the single version that clears all of them:
**`4.1.138.Final` / `4.2.18.Final`**.
> ⚠️ **Correction to v0.1.x.** Versions 0.1.x of this tool (and its README) said that
> `4.1.137.Final` / `4.2.17.Final` clears everything. **That answer is out of date.**
> It was correct for the 14 advisories that existed when it was written; the 2026-09-10 batch
> moved the target one patch release up. On `4.1.137.Final` / `4.2.17.Final` you are still exposed
> to **8** of the new advisories, including one rated **high** (`GHSA-pvjx-v7vp-62vq`, below).
> If you upgraded because v0.1.x told you to, upgrade once more.
Single jar, zero runtime dependencies, fully offline, Java 17+.
---
## What is covered
**14 CVEs in the GitHub Advisory Database** (Dependabot alerts on these):
`CVE-2025-58056` · `CVE-2025-67735` · `CVE-2026-33870` · `CVE-2026-41417` · `CVE-2026-42580` · `CVE-2026-42581` · `CVE-2026-42585` · `CVE-2026-42587` · `CVE-2026-50020` · `CVE-2026-56746` · `CVE-2026-59898` · `CVE-2026-59899` · `CVE-2026-59903` · `CVE-2026-59921`
**9 repository-only advisories, published in `netty/netty` on 2026-09-10** (no CVE ID yet; Dependabot does not alert on these):
| Advisory | Severity | What | Fixed in |
|---|---|---|---|
| `GHSA-pvjx-v7vp-62vq` | **high** | `HttpServerCodec`: unbounded per-connection queue via HTTP/1.1 pipelining → heap exhaustion | 4.1.138 / 4.2.18 |
| `GHSA-2g6j-r8q9-5hr8` | **high** (CVSS 7.3) | `HttpServerCodec` response desynchronization | 4.1.137 / 4.2.17 |
| `GHSA-2g37-3h88-55hc` | medium | `WebSocketServerExtensionHandler`: unbounded per-connection queue via pipelining | 4.1.138 / 4.2.18 |
| `GHSA-3jrc-fchc-59pw` | medium | `Transfer-Encoding` split across header fields bypasses the final-`chunked` check → request smuggling | 4.1.138 / 4.2.18 |
| `GHSA-hcvj-94mj-jp5c` | medium | Incomplete validation of malformed `Transfer-Encoding` (bypass of an earlier fix) → request smuggling | 4.1.138 / 4.2.18 |
| `GHSA-j4mg-hqgv-34qc` | medium | Whitespace after the digits in chunk-size is truncated → request smuggling | 4.1.138 / 4.2.18 |
| `GHSA-rq4j-fc47-9698` | medium | Control characters in the chunk-size line → request smuggling | 4.1.138 / 4.2.18 |
| `GHSA-h75q-xqrh-59rf` | medium | `RtspDecoder` method token with a trailing control byte | 4.1.138 / 4.2.18 |
| `GHSA-rmcw-9fcq-wjq7` | medium | `SpdySessionHandler` accepts unlimited concurrent remote streams → OOM | 4.1.138 / 4.2.18 |
In the tool's output every one of these nine is tagged `[Dependabot/全局库:未收录]`
("not in the GitHub Advisory Database").
## Why this exists
### 1. Nine of these advisories are invisible to Dependabot
Dependabot alerts come from the GitHub Advisory Database. On 2026-09-10 Netty published a batch of
security advisories in its own repository (`netty/netty` → Security → Advisories). As of 2026-09-19,
**none of the nine that affect `netty-codec-http` is in the GitHub Advisory Database** —
`gh api advisories/<GHSA-ID>` returns 404 for every one of them, while a known entry
(`GHSA-8c42-7qj2-3j46`) returns 200 as a control. None has a CVE ID yet.
So a project that Dependabot reports as clean at `4.1.137.Final` is still inside the affected
range of eight of them.
### 2. Which of the new ones actually matter to you — without overstating it
- **`GHSA-pvjx-v7vp-62vq` is the default-path one.** It is in `HttpServerCodec`, the
request-decoder/response-encoder pair that essentially every Netty HTTP/1.1 server uses. A remote,
unauthenticated client pipelines many requests on one connection and does not read the responses;
the per-connection method queue grows without bound until the heap is exhausted. No special
configuration is required. Rated `high`; the advisory publishes no CVSS score.
- **`GHSA-2g6j-r8q9-5hr8` needs three things at once**: HTTP/1.1 pipelining, a `HEAD` request in the
pipeline, and a client sending `Expect: 100-continue`. Only then can a response body be dropped for
one request and sent for another. It is rated `high` (CVSS 7.3), but it is not a default-path issue.
It was fixed in `4.1.137.Final` / `4.2.17.Final`, so the v0.1.x answer already covered it.
- **The four request-smuggling ones** (`3jrc`, `hcvj`, `j4mg`, `rq4j`) are parser-differential
issues. They matter when Netty sits behind or in front of another HTTP component that parses the
same malformed message differently.
- **`2g37`, `h75q` and `rmcw`** only apply if you use WebSocket extensions (e.g.
`WebSocketServerCompressionHandler`), `RtspDecoder`, or SPDY respectively.
### 3. Netty ships one version number, but each advisory has its own "fixed in"
Every Netty module is released under the same version number, so upgrading feels like a single
decision. It isn't. Across these 23 advisories the 4.1-line fix versions land on
**125 / 129 / 132 / 133 / 135 / 136 / 137 / 138** — eight different values. Follow any one advisory
and you are still inside the affected range of others.
### 4. If you fixed the HTTP/2 CVEs, you are probably still exposed here
`netty-codec-http2` declares `netty-codec-http` as a `compile` dependency with
`<version>${project.version}</version>` — **the two versions are pinned to each other**.
So if you upgraded to `4.1.136.Final` because that is the version which clears the seven
`netty-codec-http2` CVEs, you now run `netty-codec-http` **4.1.136.Final** too. That is inside
`CVE-2026-59903` (`<= 4.1.136.Final`) and all nine repository-only advisories. You need
**`4.1.138.Final`**. On the 4.2 line the HTTP/2 answer is `4.2.16.Final`; this side needs `4.2.18.Final`.
This is not a claim that the HTTP/2 advice was wrong — it answered for a different module.
It is a claim that **"one version number" hides the fact that you were answering for one module only.**
### 5. Version ranges are not written consistently — and one is missing its upper bound
- In the original fourteen advisories the upper bound is written `<=` 16 times and `<` 12 times.
The same `4.1.136.Final` is therefore **safe** for `CVE-2026-56746` (`< 4.1.136.Final`) and
**affected** for `CVE-2026-59903` (`<= 4.1.136.Final`). Normalising the operator either way
gives a wrong answer, so each operator is kept exactly as published.
- `GHSA-3jrc-fchc-59pw` publishes **only a lower bound** (`>=4.1.132.Final` / `>=4.2.12.Final`).
Read literally, even the fixed `4.1.138.Final` would be "affected". The generator adds the
published fixed version as an explicit upper bound for that one advisory only, prints the original
range next to it, and fails if any other range would be altered.
### And it does not scan `pom.xml` — on purpose
If you use Spring WebFlux, `netty-codec-http` arrives through
```
spring-boot-starter-webflux
-> spring-boot-starter-reactor-netty
-> reactor-netty-http
-> netty-codec-http
```
**The artifactId never appears in your `pom.xml`.** A pom-based check answers "I don't use it",
which is wrong. This tool reads the jars that actually ship.
## Usage
```bash
java -jar netty-http-check.jar target/ # scan build output
java -jar netty-http-check.jar myapp.jar # Spring Boot fat-jar / war, nested jars included
java -jar netty-http-check.jar --version-of 4.1.137.Final # judge a version directly
```
## Exit codes
| Code | Meaning |
|---|---|
| `0` | The scan finished, **every file was actually read**, and none of the 23 applies |
| `1` | Affected by at least one of the 23 |
| `2` | Could not judge (bad arguments, path missing, no `netty-codec-http` found, version not parseable) |
| `4` | **Some file could not be read** — not a zip, truncated, or an I/O failure |
🔴 `2` is deliberately not `0`: "nothing found" and "clean" must not look the same to a script.
🔴 **`4` was added on 2026-09-09.** Before that, an unreadable file only produced a line on stdout
while the exit code stayed `0` — so in CI "I could not read it" silently meant "pass".
**"I could not read it" and "you are safe" must be two different sentences.** If something affected
is found, `1` takes priority over `4`.
⚠️ A **legitimately empty jar** (a bare 22-byte EOCD record) is a real empty zip, not a broken
file, and does not trigger `4` — false alarms are what make real alarms get ignored.
## What it does not tell you
- **Only these 23 advisories, only `netty-codec-http`.** Other Netty modules have their own
advisories (the 2026-09-10 batch also covers `netty-codec-http2`, `-http3`, `-stomp`, `-smtp`,
`-mqtt` and others); a clean result here says nothing about them. If you also run
`netty-codec-http2`, the tool says so and points at the cross-module problem above, but it does not
judge that module.
- **Whether a vulnerable code path is reachable in your application.** It judges by version. §2 above
is the honest summary of which ones need specific handlers or preconditions.
- **Severity is reported as published.** Several advisories carry no CVSS v3 score (including the
`high` `GHSA-pvjx-v7vp-62vq`); they are printed as they are, not rolled up into a scary total.
- **The Dependabot gap is a snapshot.** It was true on 2026-09-19. Once GitHub imports these
advisories into its database, Dependabot will alert on them and the tag in the output will be out of
date until the table is regenerated (the generator refuses to run when that happens — see below).
## How the rule table is built
`src/main/java/dev/mikko/nettyhttpcheck/RuleTable.java` is **generated**, never hand-edited:
```bash
python tools/gen_rules.py --dry # run the assertions only
python tools/gen_rules.py # regenerate the table
```
Ten assertions must pass or nothing is written. Among them: the 14 global advisories are still
`reviewed`; the 2026-09-10 repository batch for `netty-codec-http` is still exactly these nine and
**still absent from the GitHub Advisory Database** (with a positive control that must return 200);
the computed single answer is still `4.1.138.Final` / `4.2.18.Final` and fetchable from Maven Central
(with a sentinel version that must 404); **the old v0.1.x answer `4.1.137.Final` / `4.2.17.Final`
still hits `GHSA-pvjx-v7vp-62vq`**; the lower-bound-only range is handled and no other range is
modified; the `<` / `<=` boundary case still flips; and the HTTP/2 answer from §4 still leaves a hole
on this side.
`tools/recheck_before_publish.py` re-checks the published claims before any write-up goes out; it exits
`3` if the nine advisories appear in the GitHub Advisory Database, if `4.1.138.Final` / `4.2.18.Final`
disappear from Central, or if `4.1.139.Final` / `4.2.19.Final` appear (a newer release may mean newer
advisories).
Real-jar end-to-end checks live in `tools/e2e_real_jars.py` — they download actual jars from
Maven Central (including `4.1.137`, `4.1.138`, `4.2.17`, `4.2.18`) rather than fixtures, because
"works on a hand-built zip, fails on the real jar" is a real failure mode.
## License
Apache License 2.0 — see `LICENSE`.