Java vulnerabilities of the week: 1 to 7 October 2026

A vulnerability feed gives you a CVE id, a severity, a version range and a sentence saying an attacker may cause a denial of service. The useful part is the mechanism, the length field nobody checked, the object two threads were holding at the same moment, because that’s what I can go and look for in my own code, and it’s worth far more then the score. That’s why this series exists, and the mindset behind it is in the CVE stigma post.

Most of this week is one idea in different clothes: the other side gets to decide something it shouldn’t. A Thrift peer says how many elements a list has, an LDAP server says how big the next element is and how many bcrypt rounds a password check will cost, a Struts form field picks the scale of a decimal and with it the size of the page, a request header picks where a Logback appender writes. The two deep dives are the exceptions: a Struts bug that needs no attacker at all, and XML hardening in Camel Quarkus that had no effect because the implementation underneath doesn’t support it.

The week in five

Honourable mention for Apache Commons BCEL 6.13.0, CVE-2026-94114 and CVE-2026-105111, disclosed 6 October 2026. The class repositories cached a parsed class under the this_class name inside its own bytes rather then the name it was requested by, so one class file could take over another’s cache entry, a mismatch the JDK’s defineClass() refuses. The record for 94114 is titled as recursion in ClassParser, while its description, CVSS vector and referenced commit all describe the cache issue. 105111 is a stored XSS in Class2HTML. One older item may have reached your scanner in the last few days: four Jackson denial-of-service advisories from 22 September, which entered the GitHub Advisory Database on 30 September and 1 October, fixed in 2.18.11, 2.21.7, 2.22.3, 3.1.7 and 3.2.3.

Apache Struts: a cached formatter is shared mutable state

Struts renders localized messages through an application-wide text provider. A message like Your order shipped on {0,date,long} is a java.text.MessageFormat pattern, and building a MessageFormat means parsing the pattern and creating a sub-format for each argument. Nobody wants to do that on every request, so AbstractLocalizedTextProvider.buildMessageFormat() caches the result by pattern and locale.

Up to 7.3.0 it returned the cached instance itself, the same object to every request. MessageFormat is not thread-safe, its Javadoc asks for one instance per thread or external synchronization. The date sub-format is a SimpleDateFormat, which keeps a Calendar in a field and formats a date by setting that calendar and then reading the year, month and day back out of it. Two requests formatting the same message at the same moment share that calendar, so one can set it while the other is reading, and the second prints a date that isn’t its own, a mix of the two, or throws from inside the calendar code as a server error.

I tried the JDK part on its own: one shared MessageFormat with a {0,date,yyyy-MM-dd} argument, four threads, two formatting 1 January 1999 and two formatting 7 October 2026. In one run more than half of 800,000 calls returned a wrong date and a few thousand threw ArrayIndexOutOfBoundsException. The numbers moved between runs. With a fresh copy for each call they stayed at zero.

The S2-078 bulletin says that triggering it requires no malicious request: ordinary concurrent traffic is enough, and the users harmed are the application’s own. Two customers loading the same page at the same moment is the whole trigger, and one customer’s appointment or delivery date can show up on the other’s screen. No message shipped with Struts formats a date, so the exposure is in your own resource bundles, and the workaround is to format the date yourself and pass the finished string.

Fix in 7.4.0 and 6.12.0 is one line: buildMessageFormat() now returns format.clone(), with a comment saying the cached instance is a template that is never formatted directly. Cloning a MessageFormat clones its sub-formats, and cloning a DateFormat clones its calendar, so the expensive parse stays cached and every caller gets its own state.

Why I find it interesting

This is CWE-362, a race on a shared resource, in a shape most Java web developers meet eventually: a mutable object that isn’t thread-safe, kept somewhere that outlives a request. A servlet field, a field on a Spring singleton, a static final SimpleDateFormat, and here a cache. The cache version is the easiest to miss, because a cache reads like a performance decision, and handing the same mutable object to every caller is sharing whatever the map is called. Cloning from a cached template is a good pattern for anything expensive to build and unsafe to share, and for dates java.time.format.DateTimeFormatter is immutable and thread-safe, so if you still have a SimpleDateFormat constant in code that serves requests, its worth replacing. An attacker can’t aim this bug. It shows up first as an odd support ticket, a customer seeing a date that isn’t theirs, long before anyone calls it a vulnerability.

Apache Camel Quarkus: XML hardening only counts if the implementation honours it

Camel Quarkus is part of the Camel family I work in, and this one was found by internal analysis, so it’s close to home.

JAXP is a set of interfaces with pluggable implementations. TransformerFactory.newInstance() returns whatever the JVM is configured to provide, the JDK’s built-in XSLTC, Saxon or Xalan, depending on system properties and the classpath, and hardening goes through the same interfaces. JAXP 1.5, in Java 7u40 and Java 8, added XMLConstants.ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_STYLESHEET. Set both to an empty string and the factory won’t fetch external DTDs, entities or stylesheets, which is what stops <!ENTITY x SYSTEM "file:///etc/passwd"> from pulling the file into the output. Camel sets both on the factories it creates.

An attribute is a request to the implementation, and an implementation can only honour the attributes it knows. Xalan-J 2.7.x predates JAXP 1.5. According to the fix commit, its setAttribute() throws IllegalArgumentException for both attributes, and its FEATURE_SECURE_PROCESSING limits extension functions without the external access restrictions the JDK ties to the same feature. Hardening code commonly wraps the call in a try/catch that ignores the exception, so an implementation without support doesn’t break startup, and Camel’s own XmlConverter does that after calling TransformerFactory.newInstance(). With Xalan registered as the JAXP default, that code completes without an error and returns a factory with none of the restrictions, and any other library in the application that calls newInstance() gets a Xalan factory too.

On the xslt component path only bodies that already are a javax.xml.transform.Source were exposed, since Camel converts other body types itself with external entities disabled. The fix stops asking Xalan for something it can’t do: XalanTransformerFactory now parses input documents with an XMLReader that resolves no external entities and loads no external DTDs, and refuses resources requested through the XSLT document() function unless the application’s own URIResolver resolves them. Until you upgrade, leave the body as a String, byte[] or InputStream. The advisory notes that convertBodyTo on an existing Source doesn’t help, because that conversion is an identity transform through the same factory.

Why I find it interesting

The class is XXE, CWE-611, and the lesson is how a hardening step fails open. The try/catch around the attribute call has a good reason behind it, and the consequence is that when the protection can’t be applied, execution carries on as if it had been. And the classpath picks the implementation: a dependency that registers a TransformerFactory changes what newInstance() returns for every library in the JVM, and none of them are told.

Two habits I’d keep from this. When a security setting can’t be applied, log it at warn or fail, don’t swallow it, which is what the follow-up commit does for secure processing on the input reader. And check which implementation you actually got: print TransformerFactory.newInstance().getClass() once in the running application, or ask for the JDK one explicitly, as the advisory recommends, by naming com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl. On Java 9 and later TransformerFactory.newDefaultInstance() does the same without naming an internal class. A security property is only as strong as the implementation that receives it.

What I take away from this week

First, who patches. Struts, Thrift, the LDAP API and BCEL are libraries you declare or inherit transitively, and no framework BOM will move them for you, so start from mvn dependency:tree: Struts 6.12.0 or 7.4.0, libthrift 0.25.0, LDAP API 2.1.9, BCEL 6.13.0. Camel Quarkus moves with the platform, 3.40.0, or 3.33.3 on the LTS stream. Logback needs a closer look on Spring Boot: Boot 4.1.1 and 4.0.8 manage Logback 1.5.38, the 4.2 milestones are on 1.6.x, and no 1.5 release has the fix. Most applications never configure a SiftingAppender, but if yours sifts on an MDC value from a request, the fix is 1.6.5 as an explicit, tested override, or keeping request data out of that key.

Second, look at the fixes rather than the bugs. Thrift bounds a declared element count by the bytes actually available, the LDAP API caps the bcrypt cost at 16, Struts caps fraction digits at 340 and REST bodies at 2 MB, Logback caps a discriminator value at 64 characters. Each one is a limit the receiver owns. For every length, count, cost or depth you parse off the wire, the review question is what it’s checked against, and who chose that number.

Third, the volume: 61 CVEs in one Thrift release, six in the LDAP API, four in Struts, and several advisories name a tool next to the people. The LDAP API and BCEL records credit Claude Security as a tool alongside the ASF, Thrift’s CVE-2026-61373 lists Claude (Anthropic Research) among its finders, and the Thrift summary on oss-security says it was drafted with AI assistance and reviewed and sent by Jens Geyer for the PMC. I wrote yesterday about what the human in the loop is actually doing. For a reader of advisories the practical side is simpler: with more CVEs per release, the count tells you less and the mechanism tells you more.

References