Java vulnerabilities of the week: 27 August to 2 September 2026

Last week’s post covered 19 to 25 August, this one covers 27 August to 2 September. Same format as always: five things worth knowing, then two of them taken apart properly, because the CVE number and the CVSS score are the least reusable information in any advisory and the mechanism is the most.

The theme this week is one I did not go looking for and then could not stop seeing. Nobody on this list forgot to add a check. Wicket had a resource guard and a CSRF policy, Qute had a filter on reflective calls, Jackson had a denylist, Tomcat had a constraint matcher and a realm, and Quarkus’s Spring layer had an annotation with the word “header” right there in its name. Every bug below is a control that exists and is wrong about the input it sees, or about what that input means. That is a different failure from a missing control, and it deserves a different kind of review.

A note on dates: Tomcat made its ten CVEs public on 25 August, two days before this window opens, and I promised them last week, so they are here. Everything else was disclosed inside the seven days.

The week in five

Honourable mentions. Apache Hive published three on 26 August, one day before the window: CVE-2026-53561, where a forged Authorization: Bearer token sent to /cliservice on a SAML-configured HiveServer2 authenticates as any user, plus an SSRF in the Avro SerDe and a SQL injection in the metastore, all fixed in 4.2.1. Apache Spark CVE-2026-32773 (1 September, Low) is XSS in the History Server from a malicious job, fixed in 3.5.8. And on 28 August the GitHub Advisory Database reviewed in ten Yamcs advisories and five MariaDB connector ones that their projects had published on 14 and 9 July. Yamcs compiles StreamSQL to Java at runtime with Janino, and CVE-2026-55565 (CVSS 9.9) is a LIKE pattern pasted unescaped into a Java string literal, so a quote injects a static {} block. The MariaDB headline, CVE-2026-55857, is a hostile server switching the driver to the PAM dialog plugin over plain TCP and receiving the password in the clear. Seven weeks from project disclosure to database record, the same lag I noted for GeoTools last week.

Apache Wicket: a borrowed policy keeps its author’s assumptions

Some background first. Wicket is a component framework and interaction happens through listeners: clicking a Link calls Link.onClick() on the server, submitting a Form calls Form.onSubmit(), an Ajax behaviour calls back into its component. The request that invokes a listener is, in many cases, an ordinary GET navigation. This is the fact that matters: in Wicket, a top-level GET changes state by design.

Since 9.1.0 the framework has offered ResourceIsolationRequestCycleListener as opt-in CSRF protection. It works on Fetch Metadata, the Sec-Fetch-* headers browsers attach to every request, and its default policy, FetchMetadataResourceIsolationPolicy, fits in three lines. If Sec-Fetch-Site is same-origin, same-site or none, allow. Otherwise, if the request is a “simple top-level navigation”, a GET with Sec-Fetch-Mode: navigate whose destination is not object or embed, allow. Otherwise deny.

If that reads familiar, it is because it is the reference policy from Google’s Fetch Metadata guidance on web.dev, step for step, and Wicket’s advisory says as much: it was “derived from a reference implementation written to guard static resources”. The reference policy allows top-level navigations for a good reason: deny them and no other site can link to yours. The assumption inside that allowance is that navigating to a page does not do anything, true of the resources the policy was written for and false of a Wicket listener.

So the attack is one line of JavaScript on a page the attacker controls: set window.location to a listener URL on the victim’s application. The browser performs a top-level navigation, stamps it Sec-Fetch-Site: cross-site and Sec-Fetch-Mode: navigate, attaches the session cookie because a top-level GET is exactly what SameSite=Lax still sends cookies on, and the policy says allow. Link.onClick() runs in the victim’s session, having passed through the CSRF protection the application deliberately turned on.

The second allowance is smaller and in some deployments worse. Sec-Fetch-Site: same-site means the same registrable domain but a different origin: another subdomain, or another port. The policy allowed it unconditionally, for any method, so anything less trusted on a sibling subdomain or port, a user-content host, a staging box, could invoke any listener, POST included.

The fix in 9.24.0 and 10.11.0 teaches the policy what it is protecting. isRequestAllowed now takes a RequestType, and the navigation allowance only applies to RENDER, so a cross-site link can still render a page but never reach a LISTENER. Same-site is denied unless you call setSameSiteAllowed(true). The commit that documents the change puts the boundary in one sentence: another origin may not invoke a listener on a page.

Why I find it interesting

Because the bug is not in the code. Every line of that policy does what the reference implementation does, and the reference implementation is fine. A policy encodes assumptions about the thing it guards, and when you transplant it the assumptions come along silently. The family name I would give it is the unsafe safe method: an HTTP GET that the protocol, the browser and the borrowed policy all agree is side-effect free, sitting in framework where GET has side effects by design. SameSite=Lax makes the same assumption, which is why Wicket listeners were exposed on two layers at once.

The other two structural Wicket bugs are the same shape at a smaller scale: a guard that ran on the resource path before the attacker’s segments were appended, so it validated a different string than the one used, and a limit implemented in one parser and bypassed whenever a different parser gets to the body first. Three bugs, one review question: what input does this check actually see, and is it the input the code acts on afterwards?

Quarkus Qute: a denylist that only knew about Object

Qute is the template engine in Quarkus. An expression like {order.customer.name} is resolved by a chain of value resolvers, most of them generated at build time for the types Quarkus can see. Behind them sits a fallback, the ReflectionValueResolver that EngineProducer registers, which resolves anything else by reflection: {obj.foo} becomes getFoo(), and {obj.bar('x')} becomes a method call.

A reflective resolver in a template engine needs a boundary, and Qute had one: isMethodCandidate rejected any method declared on java.lang.Object. That blocks getClass(), the road every template injection payload in Java history has started on: get a Class, get its ClassLoader, load java.lang.Runtime, done. The denylist had one entry, and it was the right entry for the classic payload.

The bypass takes a different road to the same place. Every enum has getDeclaringClass(), declared on java.lang.Enum, not on Object, so the filter lets it through, and it returns a Class. From there getClassLoader() is declared on Class, also not Object, so it passes too. The test added with the fix spells the whole chain out as one expression, {this.declaringClass.classLoader.loadClass('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('id').inputReader().readLine()}, rendered with any enum constant as the context object. Enums are everywhere in template data, so finding one in scope is not the hard part.

The precondition is the usual one for template injection: the attacker has to control template text, not just data. Most applications never render a template a user wrote. The ones that do, custom email templates, per-tenant notification formats, anything that calls Qute.fmt on a string from a form, were fully exploitable.

The fix, committed to main on 1 September, replaces the one-entry denylist with isSafe, which rejects any method declared on Object, on Class, on any ClassLoader, or on anything in java.lang.reflect. It also stops registering the {#eval} section helper by default in the standalone engine, since {#eval} turns a runtime string into a template. As of this morning the fix is in the 3.39.2 tag but not yet on Maven Central, so every Quarkus version you can download today still has the old filter.

Why I find it interesting

Because this is the oldest story in Java security and it still works. Struts and OGNL, Spring EL, FreeMarker’s ?new, Velocity, Thymeleaf: each of them are an expression language that lets a template call methods on objects, each one denylisted getClass() at some point, and each one was bypassed by somebody finding a different road to a Class. The bug class is the reflection sandbox escape, and its defining property is that the JDK has more roads to a Class than anybody can list: getDeclaringClass() on an enum, a Method or a Field, the thread’s context class loader, Class.forName. Denying entry points is a treadmill. The durable version denies destinations, which is what the Qute fix does, and the most durable version is an allowlist of types a template may touch, which is what Quarkus’s generated resolvers and @TemplateData already are. The real advice is not to lean on the reflection fallback for anything an outsider can write.

I picked this one over the other three Quarkus bugs because the Jackson advisory is the same lesson in a different package: a denylist of unsafe base types that grew by Runnable recently and by Comparable this week, with an advisory telling you to configure an explicit validator instead. Two incomplete denylists in one week, both patched by adding an entry. That is the right fix for a maintenance release, backwards compatibility is real, and the longer-term answer in both cases is the allowlist each project already offers.

What I take away from this week

Three threads.

The first is the review question this week keeps asking. Not “is there a check” but “what does the check see, and what does it assume”. Tomcat’s realm saw a database outage and returned a principal. Wicket’s resource guard saw a path that had not finished being built. Wicket’s CSRF policy saw a GET and assumed it was harmless. Quarkus’s Spring layer saw @RequestHeader and read the query string. Every one of those would pass a checklist that only asks whether the control is present.

The second is that denylists on reflection and deserialization are still being extended one entry at a time in 2026. Jackson’s own advisory tells you to configure an explicit validator instead, and the Qute equivalent is to keep the reflection fallback away from anything an outsider can write. If you use @JsonTypeInfo on a broad base type, or you render templates a user can edit, this is the week to go and do that, independent of any version bump.

The third is where the fixes actually reach you. Jackson will arrive through a BOM bump in Spring Boot, Quarkus or Camel, and the patched lines were on Central before the advisory, so this one is the system working. Tomcat, for most people, is whatever Spring Boot’s BOM says, so watch the next Boot patch; if you run it standalone, patch now, and read your web.xml constraint order regardless. Wicket and Shiro are not in any of those BOMs as far as I know, so their users patch directly, or for Shiro set the two resubmit system properties. Quarkus is the one to watch: three High advisories, one of them code execution, and at the time of writing the fixed releases were not on Central yet. The OIDC bug needs multi-tenancy plus a token cache you enabled on purpose, the Spring Web one needs the Quarkus REST stack and security logic keyed on headers, and the Qute one needs untrusted template text. If none of those describe you, you have time. If one does, you do not, and the version number will not tell you which.

References