Java vulnerabilities of the week: 9 to 15 October 2026

Every CVE arrives with a number and a severity, and if that is all you read you have learned nothing you can act on. What I want from a vulnerability is the mechanism: the field that was trusted, the lookup that ran twice, the check that was skipped. That is the part I can go and search for in code I maintain, and it is worth far more then the number beside it. It is why I write this up every week, and the reasoning behind the series is in the CVE stigma post.

This is the seven days to 15 October 2026, and one shape runs through most of it. A lot of this week’s bugs are a security check that looked carefully at one thing and then let the code act on a different thing. Apache CXF cached a “this token is valid” decision under a 32-bit hash and trusted the hash. Apache Fesod checked the address an image URL resolved to, then opened a connection that resolved it again. A CXF transport validated a TLS certificate chain and never looked at the hostname on it. In each one the gap is between what was validated and what was used. The two I pull apart below are the clearest examples.

The week in five

Honourable mention for the RabbitMQ Java client (com.rabbitmq:amqp-client), CVE-2026-106121, CVE-2026-106122 and CVE-2026-106123, CVE records 6 October 2026, fixed across 5.35.0, 5.36.0 and 5.37.0. A JSON reader that did not stop at end of input looped forever on an unterminated string or comment, a short-string decoder turned malformed UTF-8 into replacement characters that could re-encode past the AMQP shortstr limit, and a failed AMQP URI parse put the raw URI, which can carry a username and password, into the wrapped exception and the logs. One dating note while you are triaging: Hazelcast CVE-2026-107725 and CVE-2026-107726 reached the GitHub Advisory Database on 8 October, but the repository advisory is dated 4 August, so that one is older than the listing date suggests.

Apache CXF: a cache hit is not proof of validation

CXF can sit in front of a Security Token Service, the WS-Trust component that issues and validates SAML tokens for a set of services. Validating a token is not cheap: a signature check, a trust decision on the issuer, sometimes a round trip to the STS itself. Doing that on every request when the same token keeps coming back is wasteful, so STSTokenValidator and the STS kept a cache of tokens they had already validated.

The cache was keyed on a hash of the token. The hash was a 32-bit value from Java’s ordinary hashCode machinery, Arrays.hashCode and hashCode(), with no cryptographic properties. And the code treated a hit in that cache as proof that the presented token had already passed validation. So the check became: compute the 32-bit hash of the incoming token, and if a token with that hash is in the cache, accept it.

A 32-bit hash has about four billion possible values, and hashCode is designed to spread entries across buckets, not to resist anyone trying to land on a particular value. An attacker who can get one legitimate validated token into the cache, or who knows one is there, can build their own token whose hashCode collides with it. The advisory gives two shapes: a UsernameToken, or a SAML assertion the attacker signed themselves. That crafted token hits the cache, and because a hit stands in for the real check, it is accepted with no password verification, no trust decision on its signature, and no contact with the STS. From there the attacker is authenticated as whoever the colliding entry belongs to, and by calling the STS validate and renew operations they can get tokens the STS signs for that identity.

The fixed releases are 4.2.4, 4.1.9 and 3.6.13. The advisory points you to the upgrade and does not publish the code of the change, so I will not guess at the new key beyond that.

Why I find it interesting

The class is improper authentication, CWE-287, reached through a security decision that relied on a value an attacker can forge, which is the territory of CWE-807. Two lessons come out of it.

The first is narrow and worth saying plainly: hashCode() is a bucket selector. It returns a 32-bit int, it makes no promise about collisions, and for many types an attacker can influence it. It is not an identity and it is not a token. A cache that records “this credential was validated” has to be keyed on the credential itself, or on a cryptographic digest of it, so that a hit means the same bytes and not merely the same 32-bit number.

The second is the general one, and it is the theme of the week. A cache placed in front of a security check becomes part of the security boundary. If a cache hit lets you skip the check, then producing a cache hit is exactly as powerful as passing the check, and the key has to be as hard to forge as the check is to pass. Here the check was “is this token genuinely valid” and the thing actually tested was “does this token’s 32-bit hash match an entry”, and those are not the same thing. When you cache the result of an expensive validation, the question to ask in review is whether the cache key is the thing you validated, or a cheaper stand-in that someone else can choose.

Apache Fesod: the address you check is not the address you connect to

Fesod reads and writes spreadsheets, and one of its features embeds images that a spreadsheet references by URL. UrlImageConverter takes a URL, fetches it, and drops the bytes into the sheet. If any part of that URL comes from data your application did not fully control, the fetch is a server-side request you did not mean to make, which is SSRF: the classic targets are cloud metadata endpoints like 169.254.169.254 and internal services on 127.0.0.1 or the private ranges.

That was reported in June as CVE-2026-49328 and fixed in 2.0.2. The fix resolved the URL’s hostname and checked the address it pointed at before allowing the fetch. The problem this week, CVE-2026-107460, is that the validation and the connection are two separate acts, and each resolves DNS on its own. The converter resolves the hostname, inspects the address, and decides it is allowed. Then it hands the original hostname to the HTTP client, which resolves it again to open the socket. Between those two lookups the answer can change, because the attacker owns the name and can serve a different record to the second query: a public, allowed address for the validation, and 169.254.169.254 for the connection. This is DNS rebinding, and it is a time-of-check to time-of-use gap (CWE-367) wrapped around an SSRF (CWE-918).

The fix in 2.1.0-incubating changes the posture rather than pinning the lookup. Remote image fetching is now off by default: an empty allowlist means the converter makes no network call at all. When an application does want it, it has to pass an explicit allowlist of hosts through allowedHosts(...), matched exactly, with no implicit trust of subdomains, wildcards rejected, and the allowlist validated before any DNS lookup, with each redirect hop checked again. Private-host and private-range checks remain. If your code uses UrlImageConverter with remote URLs, upgrading will stop the fetches until you opt back in, so its worth finding those call sites before you bump the version.

Why I find it interesting

DNS rebinding is one of those bugs that reads as obvious once you see it and is easy to ship anyway, because the validate step and the use step are written as separate lines and a hostname looks like a stable thing. It is not. A name is a question you ask DNS, and you can ask it twice and get two answers. So resolving a hostname to validate it, and then connecting by the same hostname, validates nothing about the connection: you checked one answer and you are using another.

The robust version of this fix resolves the name once and connects to that exact address, or checks the address of the socket after it is established, so the thing you validated and the thing you talk to are the same object. Fesod’s allowlist does something different and still useful: it shrinks the set of hosts that can be aimed at the converter down to names you listed on purpose. It narrows who can target the bug rather than closing the two-lookup window, and the residual advice follows from that, which is to allowlist only hosts whose DNS and endpoints you actually trust. Its the same shape as the CXF cache, seen from the other side. There the thing checked, a 32-bit hash, was not the thing that mattered, the token. Here the thing checked, an IP address, was not the thing used, the connection. Any check-then-use pattern has this trap in it, and the only safe form checks and uses the same handle.

What I take away from this week

First, who actually applies these. CXF, async-http-client, lz4-java and the RabbitMQ client are libraries you declare or inherit transitively, and no framework BOM will move them for you, so start from mvn dependency:tree: CXF 4.2.4, 4.1.9 or 3.6.13, async-http-client 3.0.14 on the 3.x line or 2.16.1 on 2.x, at.yawk.lz4:lz4-java 1.11.4, com.rabbitmq:amqp-client 5.37.0. Camel Karavan is an application you run, so upgrade the install to 4.22.1. Fesod is young and incubating, 2.1.0-incubating, and because its image fetching is now off by default the upgrade changes behaviour: anything relying on remote images has to opt back in with an allowlist. I checked each of these against Maven Central and the fixed versions are all published and downloadable.

Second, the thread that ties the week together: the distance between what a check looked at and what the code then trusted. A resolved address against the connected socket in Fesod. A 32-bit hash against the actual token in the CXF STS. A certificate chain against the unchecked hostname in the CXF Netty transport. A signed element against the processed element in the XML signature wrapping bug. The transport a cookie says it needs against the transport it arrived on in AHC. When you review your own code for this, the question is simple to state: is the thing I validated the exact thing I then act on, or a cheaper proxy for it that someone else gets to vary between the two moments?

Third, AI-assisted discovery keeps turning up in the credits. The CXF authentication bypass and the Karavan Kubernetes issue are credited to MopMonk-AI, and the CXF XML signature wrapping and Netty transport advisories name Claude agents used to study the security of open source projects, with a human finder, Guanping Zhang, named on the signature wrapping one as well. That continues the pattern I wrote about in is there really a human in the loop. For someone reading advisories to protect their own code the practical effect is the same as it was last week: the mechanism is the part you act on, whoever or whatever found it.

References