Java vulnerabilities of the week: 29 October to 4 November 2026

One of this week’s ZooKeeper advisories comes down to a question every SASL server has to answer: this client proved it is one principal, may it act as the one it asked for? The 8.6 score gives you nothing to look for in your own code; the question does. That is the point of this series, the mechanism you can carry into a code review, and the longer argument is in the CVE stigma post.

This is 29 October to 4 November 2026, and it is mostly Apache: ZooKeeper, Wicket, ActiveMQ and Syncope published more than thirty CVEs between them. MariaDB Connector/J starts three days before the window, and its dated where it comes up.

The theme is revocation. In ZooKeeper a request authorized just before an ACL change could commit just after it, in ActiveMQ a connection kept its write access through a live ACL update, and in Wicket a WebSocket connection outlived the session it was opened with. The deep dives are the ZooKeeper SASL bug, a code one, and the Wicket batch, a design story about one enforcement point and the routes around it.

The week in five

A few smaller ones. Apache Thrift CVE-2026-120135 (Important, 31 October): before 0.24.0, TProtocolUtil.skip() recursed with a default depth of Integer.MAX_VALUE; 0.24.0, out since July, takes the limit from TConfiguration (64 by default) and turns setMaxSkipDepth() into a no-op. Apache MINA CVE-2026-118636 (9.8, 31 October): JavaNativeMessageDecoder in the 3.0 milestones deserializes with no class filter and will not be fixed, so use 2.2.x. Apache Juneau 9.2.1 (3 November) fixes nine issues. On 30 October the reporter of the PLC4X OPC UA CVEs from 30 September explained them: verify() threw on a valid signature (a missing negation) and was handed the pre-decryption length, so together a forged signature passed, and fixing the negation alone would reject legitimate responses. Only 1.0.0 is fixed, and Camel moves to it in 4.24.0 (CAMEL-25617). And on 30 October Oracle added products that embed Java, Oracle Database Server among them, to the eMudhra alert from last week’s post.

ZooKeeper: the identity a client asks for is not the one it proved

SASL keeps two identities apart on purpose: the authentication ID the client proved, a Kerberos principal with GSSAPI, and the authorization ID (authzid) it wants to act as, which exists so a gateway can act for a user. A Java server decides in its CallbackHandler, which gets an AuthorizeCallback holding both strings and answers with setAuthorized(boolean), optionally naming a canonical identity with setAuthorizedID(). In the JDK’s GSSAPI server the authzid is whatever the client put in its last token, and the callback resolves the result like this:

public String getAuthorizedID() {
    if (!authorized) {
        return null;
    }
    return (authorizedID == null) ? authorizationID : authorizedID;
}

So if the handler says yes and names no identity, the session gets the identity the client asked for. ZooKeeper adds it as new Id("sasl", authorizationID), plus super when it matches zookeeper.superUser.

The client port handler called ac.setAuthorized(true) first, then canonicalized the authentication ID with KerberosName.getShortName() and passed the result to setAuthorizedID(). For a principal in the default realm that overwrites the requested authzid, and the session gets the proved identity. For any other realm, under the default auth_to_local rule, getShortName() throws NoMatchingRule; the handler logged the IOException and returned, still authorized and with no authorized ID. A client authenticated as bob@CROSSREALM (the case in the fix’s own test) that asks for the superuser name gets the whole tree.

The fix, commit 0afc927, compares the two IDs first and denies on a mismatch, the same authenticationID.equals(authorizationID) check the quorum port’s SaslQuorumServerCallbackHandler makes.

Why I find it interesting

It is improper authorization, CWE-285, in the form where the identity used for access control is not the one that was authenticated. Each line reads correctly on its own, authorize, canonicalize, log the failure; the bug is the order plus a JDK default, and in the common same-realm setup it never shows.

Two habits follow. Write a security decision last, after everything that can throw, with deny as the default. And treat a SASL principal as two strings: if you maintain a SASL server in Java, grep for AuthorizeCallback and check what happens when they differ. Without proxy users the rule is equality; with them it is an explicit list of who may act as whom.

Wicket: an enforcement point covers only the routes that pass through it

Wicket’s IAuthorizationStrategy holds an application’s access rules and answers three questions: may this class be instantiated, may this component RENDER or ENABLE, and may this resource be served. The rules live in one place and the framework asks at the right moment. Most of this batch is about the moments.

The resource question is asked in one place, the constructor of ResourceRequestHandler, built for requests to a mounted or shared ResourceReference. But ResourceLink, Image, ImageButton and AjaxDownloadBehavior can also be handed an IResource, and then they served it themselves:

Attributes a = new Attributes(RequestCycle.get().getRequest(), RequestCycle.get()
    .getResponse(), null);
resource.respond(a);

So the constructor decided whether the check ran: new ResourceLink(id, reference) was authorized and new ResourceLink(id, resource) was not. ExportToolbar uses the second form (CVE-2026-82705). The WebSocket transport called onPayload() on a shared WebSocketResource directly, so @AuthorizeResource had no effect there (CVE-2026-82115).

RENDER had its own way around. The outcome of the RENDER check is stored in a visibility flag that only Component#configure() writes, during rendering. A bookmarkable listener URL with an empty page id makes Wicket build a fresh page and invoke the listener before any render pass, so the flag still says visible and a denied component’s listener runs (CVE-2026-82704). For stateless pages Wicket writes such URLs itself.

The fixes make each route ask the strategy. ResourceLink.onRequest() now responds through new ResourceRequestHandler(resource, resourceParameters), which in the commit’s words “keeps one enforcement point rather than adding a second one that can drift”; the WebSocket transport asks about the resource a client connects to; and a fresh page gets its RENDER check before the listener runs.

Why I find it interesting

This is missing authorization, CWE-862, measured against complete mediation, every access to every object checked, as Saltzer and Schroeder put it in 1975. A single enforcement point is the right way to get there, and the fix kept it rather then adding checks to each component. The review question is which routes reach the object without passing through the check. Thats a question about entry points, here a second constructor, a second transport and a request that skipped a lifecycle phase, and it fits any Java stack: a filter on /admin/* does not see the same handler reached through a WebSocket, a message listener or a scheduled job.

The RENDER case adds that a decision stored as state is only as current as the code that last wrote it, which is the revocation theme one level down. Fifteen of the nineteen advisories credit Emond Papegaaij, who sent the announcement for the PMC.

What I take away from this week

First, revocation. ZooKeeper’s prepared request, ActiveMQ’s per-connection write cache and Wicket’s session copy from the handshake each held a permission longer than the policy did. When code keeps an authorization decision on a long-lived object, find the event that should invalidate it and test it: revoke, then try the old connection. Its the cheapest test I know for all three.

Second, identity taken from a field the client fills in: the ZooKeeper authzid, the connection id in an ActiveMQ ConsumerId, the upload limits Wicket read from request parameters (CVE-2026-84170). In each case the server already knew the answer from its own state, and the fix uses that.

Third, where the fixes land. ZooKeeper, ActiveMQ, Wicket, Syncope and Juneau are direct upgrades, and all five ActiveMQ items are in the broker. Camel main has moved to ZooKeeper 3.9.7, PLC4X 1.0.0 and MariaDB Connector/J 4.0.0 for 4.24.0. MariaDB is the one to act on now: Spring Boot 4.0.9 and 4.1.2 and Quarkus 3.40.1 manage 3.5.10, so override it to 3.5.11 (mariadb.version in Spring Boot) until the next patch releases. If you don’t use LOAD DATA LOCAL INFILE, allowLocalInfile=false also works, since 3.5.9 enforces it for server-initiated requests (CVE-2026-61700, July). Thrift users on 0.24.0 or later, Camel included, already have that fix. Every fixed version named here is on Maven Central; I checked.

References