Java vulnerabilities of the week: 24 to 30 September 2026

Security feeds give you a CVE id, a score, and one line: an attacker may bypass authentication. As a developer that sentence is useless, because the part I can actually learn from is how the bypass works, the exact check that got skipped, the exact character that changed a query. See the shape once and you catch it in your own code, which is worth far more then the score. That is the whole point of this series, and the mindset is in the CVE stigma post.

This week has a clear thread: authentication and authorization broke across a cluster of Apache libraries and platforms. Apache MINA SSHD shipped two critical bugs, an SSH signature check that could be skipped and an LDAP filter that let the username * with password * log in. Karaf’s login modules had the same LDAP escaping gap. Roller’s OAuth endpoint trusted an identity the request handed it. Polaris let a table writer point the server’s storage client at a host of their choosing.

The connecting idea is worth holding onto: any input that feeds a security decision, an LDAP filter, an identity claim, a signature verification, a storage endpoint, is adversarial until you escape it or verify it. Two of the five items are the same bug class, LDAP filter injection, in two unrelated projects the same week, which is a good reminder of how quietly that one hides.

The week in five

Honourable mention for Apache XMLSchema, CVE-2026-102495 through CVE-2026-102497, disclosed 29 September 2026 and fixed in 2.3.3: three schema-processing denial-of-service bugs (unbounded import and include recursion, deeply nested structures, and cyclic definitions in the schema walker), found with Claude agents. XMLSchema sits under CXF and Axis2, so it arrives transitively. Apache DolphinScheduler also shipped a run of authorization and command-injection fixes across the 24th and the 29th.

Apache Karaf and MINA SSHD: an LDAP filter is a query, and a username is not data

LDAP directories are a common authentication backend, and to check a login the code runs a search: find the entry whose uid equals the username, then bind with the password. The search is expressed as an LDAP filter, a small language of its own, (uid=alice) or (&(objectClass=person)(uid=alice)), and that filter is usually assembled by dropping the username into a template like (uid=%s). There is the problem, and it is the same one as SQL: if the username is treated as text and pasted straight in, a username that contains filter syntax changes the filter itself.

Karaf’s CVE-2026-90979 is that bug in the JAAS LDAP login modules. The filter was built by substitution, and the only character escaped was the backslash. RFC 4515, the grammar for LDAP search filters, requires escaping four more: *, (, ), and the NUL byte. Two paths reached the filter without the fuller escaping, the GSSAPILdapLoginModule passing the name straight from a NameCallback, and LDAPBackingEngine.listRoles() passing principal.getName(). The other modules were fine because they escaped before calling in. A username of * turns (uid=*) into “match any entry”, and characters like ( and ) let a value close the current clause and add its own, so depending on the deployment’s filter an attacker can match entries they should not, pick up a role, or slip past the lookup. Fixed in 4.4.12.

MINA SSHD’s CVE-2026-94053 is the same bug class in an authentication path. The optional sshd-ldap component authenticates against a directory, it did not escape filter metacharacters, and so a username of * with a password of * authenticated: the * matches every entry, and the check meant to identify one user matched all of them. It is rated critical, and the fix in 2.20.0 and 3.0.0-M6 escapes filter parameters per RFC 4515.

Why I find it interesting

This is LDAP injection, CWE-90, the directory cousin of SQL injection: input crosses from data into the structure of a query because it went in as text. Most Java developers have the SQL version burned in and reach for a PreparedStatement without thinking, but the LDAP version is far less familiar, and there is no prepared-statement equivalent in the raw JNDI API, so you escape by hand against RFC 4515 or lean on a library helper that does. The place it hurts most is exactly where both of these bugs live, the authentication filter, because a single * turns “the entry whose uid is this” into “any entry”, which is not a data leak, its a login as whoever the directory returns first. Two Apache projects shipped this same fix in the same week, which shows how easy it is to escape one metacharacter and miss the other four. If you build an LDAP filter anywhere, the rule is the whole RFC 4515 set, *, (, ), \, and NUL, on every value and every path.

Apache MINA SSHD: an authentication that answered before it verified

SSH public-key authentication is a proof of possession. The client says it is alice and offers a public key, the server sends a challenge, the client signs the challenge with the matching private key, and the server verifies that signature against the public key and accepts only if it checks out. The entire security of the scheme is in that verification step. Hostbased authentication works the same way with a host key.

MINA SSHD supports asynchronous authentication: instead of deciding a login attempt inline and returning a yes or no, a server can defer, hand back a future, and complete the decision later, which is useful when the check involves a slow backend. Using it takes explicit code, and most servers never touch it.

CVE-2026-77185 is a flaw in that asynchronous path. For public-key and hostbased authentication it could skip the signature check altogether, or return the wrong result. So on a server that opted into async auth, the verification that is the whole point of public-key authentication might not run, and a client offering a public key without the matching private key could be accepted. Its rated critical at CVSS 9.1, and the reason it is not scored higher is that it needs the server to use the asynchronous feature, which is uncommon. The fix in 2.20.0 and 3.0.0-M6 corrects the logic and also forbids asynchronous authentication with the public-key and hostbased schemes, so the dangerous combination cannot be assembled at all.

Why I find it interesting

The lesson is about where a security check sits in the control flow. An authentication result and the verification it depends on cannot be separable: the moment “have we verified the signature” and “do we return success” are two steps that can be reordered or short-circuited, a bug between them becomes an authentication bypass. Making the check asynchronous introduced a third state, “not decided yet”, and the flaw was that this state could resolve to something other than a hard “no”. That is the broken-authentication family, and specifically a state-machine ordering flaw: the safe invariant is that the default at every branch is denial, and success is reachable only through the verify step, never around it. I also like the shape of the fix, because it does not only correct the logic, it removes the risky combination, async plus signature-based auth, entirely. When a feature and a security guarantee are hard to hold together, taking the feature off that path is often the honest fix, and it is a pattern worth recognising: the strongest patch is sometimes the one that deletes an option.

What I take away from this week

First, who patches. These are things you run or embed, not transitive libraries a BOM bump will quietly carry, with one big exception. Apache MINA SSHD is that exception: it is embedded in a lot of Java software that speaks SSH or SFTP, so you may be shipping it without having chosen it, and fix is a resolved-dependency bump. Check org.apache.sshd:sshd-core in your tree and get to 2.20.0 or 3.0.0-M6. Karaf, Qpid Broker-J, Roller and Polaris are servers you operate: Karaf to 4.4.12, Qpid to 10.1.1, Roller to 6.1.6, Polaris to 1.8.0.

Second, the LDAP escaping gap is the thing to act on beyond patching, because it is in your own code too if you touch LDAP. Two projects shipped the same RFC 4515 fix this week, and the failure in both was identical: escape the backslash, miss the * and the parentheses. There is no PreparedStatement for LDAP filters, so if you assemble one anywhere, escape every value against the full RFC 4515 set or use a helper that does, rather then escaping one character and moving on, and treat the authentication filter as the highest-stakes place to get it right.

Third, the week’s real theme is that authentication input is adversarial. An LDAP filter, an OAuth identity, an SSH signature, a storage endpoint: each is data that decides who you are or what the server will reach, and each of these bugs is what happens when that data is trusted a step too early. Escape it or verify it before it makes the decision, default every branch to denial, and be wary of any path where the check and the outcome are not welded together.

References