CVE-2026-34486: one moved line turned Tomcat's cluster encryption into RCE

Tomcat's EncryptInterceptor fail-open regression forwards undecrypted cluster bytes to Java deserialization on port 4000.

Read time
14 min
Word count
2.3K
Sections
14
FAQs
8
Share
Dark hero graphic reading The fix that failed open, summarising Tomcat Tribes CVE-2026-34486
A one-line regression moved super.messageReceived() outside its try block, opening an unauthenticated deserialization sink.
On this page · 14 sections
  1. Why this one is worse than a normal Tomcat CVE
  2. How Tribes clustering works, in three sentences
  3. The one-line regression, shown
  4. The attack chain, at the level you need to defend it
  5. Step one: find out if you are exposed
  6. Step two: patch to the fixed release
  7. Step three: harden the deserialization sink anyway
  8. The Kubernetes trap
  9. Is it actually being exploited
  10. India-specific considerations
  11. What to change so the next one is cheaper
  12. FAQ
  13. How eCorpIT can help
  14. References

Summary. CVE-2026-34486 is a fail-open regression in Apache Tomcat's Tribes clustering, disclosed on 9 April 2026 and added to CISA's Known Exploited Vulnerabilities catalog on 4 August 2026 at CVSS 7.5. The bug lives in EncryptInterceptor, the component operators enable specifically to encrypt cluster traffic. A single line, super.messageReceived(msg), was moved outside its try/catch block during the 13 March 2026 fix for a separate padding-oracle bug (CVE-2026-29146). After that move, a message that fails to decrypt is no longer dropped: the original attacker-controlled bytes are forwarded up the chain to XByteBuffer.deserialize(), a bare ObjectInputStream.readObject() with no ObjectInputFilter. Any host that can reach the Tribes receiver, TCP port 4000 by default, can send a serialized Java object; with a gadget library on the classpath, that is unauthenticated remote code execution. Affected versions are exactly three: 11.0.20, 10.1.53 and 9.0.116. Fixed releases 11.0.21, 10.1.54 and 9.0.117 shipped between 2 and 4 April 2026. Tomcat 8.5.x does not have EncryptInterceptor and is not affected. The Hacker News reported on 5 August 2026 that exploitation of CVE-2026-34486 was tied to an autonomous AI-driven campaign that Palo Alto Networks Unit 42 attributes to an actor operating as knaithe. This guide shows how to tell whether you are exposed, what to patch, and how to harden the deserialization sink even after you patch.

This is a developer and infrastructure guide, current as of 5 August 2026.

Why this one is worse than a normal Tomcat CVE

Most Tomcat advisories affect a feature you can reason about from the outside. This one has three properties that make it nastier.

First, it only bites people who did the secure thing. EncryptInterceptor is opt-in. You add it because you want cluster replication traffic encrypted with a pre-shared key. The regression is in the receive path of that exact component, so the population at risk is the subset of operators who already cared about cluster security.

Second, the vulnerable window is narrow and specific. Exactly three point releases are affected, all shipped in a two-week window in March 2026. If you upgraded Tomcat during that window and pinned, you may be sitting on a vulnerable build without ever having touched the clustering config again.

Third, the sink is Java deserialization, which converts "you can send bytes to a port" into "you can run code" whenever a usable gadget class is on the classpath. Spring, Hibernate and many common frameworks pull in transitive dependencies that have historically shipped gadget classes. You do not have to have added commons-collections yourself for it to be reachable.

How Tribes clustering works, in three sentences

Tomcat's Tribes framework replicates HTTP session state across cluster nodes. When a session changes on one node, the change is serialized and broadcast over TCP to the others, and the receiver listens on port 4000 by default, bound to the primary network interface rather than localhost. The wire envelope has a fixed 7-byte header (FLT2002), a length, the payload and a 7-byte footer, with no cryptographic protection or authentication on the framing itself.

That last clause is the precondition for everything below. Any host that can open a TCP connection to the receiver can submit a well-formed Tribes message. EncryptInterceptor was the gate that was supposed to make the contents safe.

The one-line regression, shown

Striga, the firm that found and reported the bug, published the before-and-after. In Tomcat 11.0.18, EncryptInterceptor.messageReceived() calls super.messageReceived(msg) inside the try block:


            public void messageReceived(ChannelMessage msg) {
    try {
        byte[] data = msg.getMessage().getBytes();
        data = encryptionManager.decrypt(data);
        XByteBuffer xbb = msg.getMessage();
        xbb.clear();
        xbb.append(data, 0, data.length);
        super.messageReceived(msg);
    } catch (GeneralSecurityException gse) {
        log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse);
    }
}
          

If decrypt() throws, control jumps to the catch, the error is logged, and the method returns. The message is dropped. That is fail-closed.

The 13 March 2026 fix for CVE-2026-29146 restructured the encryption manager to support AES/GCM/NoPadding and move away from the padding-oracle-prone AES/CBC/PKCS5Padding. In the process, the call moved one line down, outside the catch:


            public void messageReceived(ChannelMessage msg) {
    try {
        byte[] data = msg.getMessage().getBytes();
        data = encryptionManager.decrypt(data);
        XByteBuffer xbb = msg.getMessage();
        xbb.clear();
        xbb.append(data, 0, data.length);
    } catch (GeneralSecurityException gse) {
        log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse);
    }
    super.messageReceived(msg);
}
          

Now super.messageReceived(msg) runs unconditionally. On a decrypt failure the catch logs the error, then the original, unmodified, attacker-controlled bytes go up the interceptor chain anyway. They reach GroupChannel.messageReceived(), which calls XByteBuffer.deserialize(), which creates a plain ObjectInputStream with no class filter and calls readObject().

The send path in the same class throws ChannelException on encryption failure, so it stays fail-closed. The asymmetry, fail-closed on send and fail-open on receive, is accidental, and it is the whole vulnerability.

The attack chain, at the level you need to defend it

You do not need to write the exploit to defend against it, but you should understand the five steps so your controls map to them.

Step What happens Where you can break it
1. Reach the port Attacker opens TCP to the receiver (default 4000) Network segmentation, firewall, NetworkPolicy
2. Frame a message Attacker builds a raw Tribes envelope with a fake member address; no auth on the framing Not defensible at this layer
3. Fail decryption EncryptInterceptor throws, logs "Failed to decrypt message" Patch restores the drop here
4. Forward the bytes Regression forwards attacker bytes to XByteBuffer.deserialize() Patch; and ObjectInputFilter blocks the class
5. Deserialize a gadget ObjectInputStream.readObject() runs a gadget chain to Runtime.exec() Remove gadget libraries; JVM-wide deserialization filter

The single SEVERE log line that Striga observed is worth memorising, because it is what a defender sees on a real attempt:


            SEVERE [Tribes-Task-Receiver[Catalina-Channel]-1]
  org.apache.catalina.tribes.group.interceptors.EncryptInterceptor.messageReceived
  Failed to decrypt message
    javax.crypto.AEADBadTagException: Tag mismatch
          

No deserialization error follows it. On a vulnerable node the gadget executes silently after that line. So a Failed to decrypt message entry that correlates with an unexpected inbound connection to port 4000 is not noise, it is your incident.

On JDK 8u72 and later the classic CommonsCollections1 and CC3 chains are broken, but CC6 uses HashSet as its entry point and works on JDK 17 and 21. Modern runtime does not save you here.

Step one: find out if you are exposed

Run these three checks in order. You are only at risk if all three are true: an affected version, EncryptInterceptor configured, and the receiver reachable beyond your trusted cluster members.

Check the running version:


            $CATALINA_HOME/bin/version.sh | grep "Server number"
          

If it reads 11.0.20, 10.1.53 or 9.0.116, you are on an affected build. Anything at or above 11.0.21 / 10.1.54 / 9.0.117 is fixed. Anything below the affected builds (for example 11.0.18) never had the regression. Tomcat 8.5.x is not affected at all.

Check whether EncryptInterceptor is actually in the channel. It lives in server.xml (or a context's cluster config):


            grep -rn "EncryptInterceptor" $CATALINA_HOME/conf/
          

No match means the vulnerable code path is not in your interceptor chain, and this CVE does not apply to you even on an affected version. A match means you are in scope.

Check whether the receiver port is listening and on which interface. The default is 4000, but read the Receiver element's port attribute to be sure:


            grep -rn "Receiver" $CATALINA_HOME/conf/server.xml
ss -ltnp | grep -E ':4000|:400[0-9]'
          

If that socket is bound to 0.0.0.0 or a routable address rather than a private cluster-only interface, treat it as reachable and move fast.

Step two: patch to the fixed release

The fix restores fail-closed behaviour by putting super.messageReceived(msg) back inside the try block. Upgrade within your branch:

Branch Affected build Fixed build Fixed release date
11.0.x 11.0.20 11.0.21 4 April 2026
10.1.x 10.1.53 10.1.54 2 April 2026
9.0.x 9.0.116 9.0.117 3 April 2026
8.5.x not affected not applicable component absent

These are minor patch upgrades within a branch, so for most deployments this is a binary swap and a restart, not a migration. Do it first. The hardening below is defence in depth, not a substitute.

Step three: harden the deserialization sink anyway

Patching closes this specific regression. It does not change the underlying design fact that a decrypted, trusted Tribes message still lands in a readObject() with no filter. A future bug in the same path would land in the same sink. Two durable controls are worth adding.

Set a JVM-wide deserialization filter so untrusted classes cannot instantiate even if bytes reach ObjectInputStream. From JDK 9 onward, jdk.serialFilter accepts an allow and deny pattern. A blunt but effective starting point blocks the common gadget packages and rejects anything not on an allowlist:


            # In setenv.sh
JAVA_OPTS="$JAVA_OPTS -Djdk.serialFilter=!org.apache.commons.collections.functors.*;\
!org.apache.commons.collections4.functors.*;\
!org.codehaus.groovy.runtime.*;\
!org.springframework.beans.factory.ObjectFactory;\
!com.sun.org.apache.xalan.**;maxdepth=20;maxrefs=1000"
          

Test this against your own replication traffic in staging first, because an over-tight filter will break legitimate session objects. The goal is to deny known gadget entry points while allowing your own serialized session classes.

Remove gadget libraries you do not need. If commons-collections-3.1.jar is sitting in $CATALINA_HOME/lib/ and nothing you run needs it, delete it. Inventory the classpath for the usual suspects:


            find $CATALINA_HOME -name "*.jar" | xargs -I{} sh -c \
  'unzip -l "{}" 2>/dev/null | grep -qE "InvokerTransformer|TemplatesImpl" && echo "{}"'
          

A hit does not mean you are exploitable, but it tells you which library provides the gadget, so you can decide whether it belongs there.

Bind the receiver to a private interface and firewall the port. The receiver should never be reachable from outside the cluster. Set the Receiver address to a dedicated cluster interface, and restrict port 4000 to peer nodes with a host firewall or security group.


            <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
          address="10.0.10.5"
          port="4000"
          selectorTimeout="5000"
          maxThreads="6"/>
          

The Kubernetes trap

In a Kubernetes deployment without a NetworkPolicy, every pod in the same namespace can reach port 4000 on every other pod. That turns a design assumption ("only trusted cluster members reach the receiver") into a false one, because the trust boundary is now the whole namespace, including any pod an attacker lands in first.

If you run Tomcat clustering in Kubernetes, a default-deny NetworkPolicy that only permits port 4000 between the cluster's own pods is not optional hardening, it is the control that makes the receiver's trust assumption true again:


            apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tomcat-tribes-restrict
spec:
  podSelector:
    matchLabels:
      app: tomcat-cluster
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: tomcat-cluster
      ports:
        - protocol: TCP
          port: 4000
          

Is it actually being exploited

The honest answer changed over the life of this CVE, and both readings are worth stating.

When SOCRadar wrote it up in April 2026, there was no confirmed in-the-wild exploitation: no KEV entry, no vendor statement, no incident reporting tied directly to CVE-2026-34486. The reasonable posture then was "treat exposed receivers as urgent regardless."

That posture is now backed by evidence. CISA added CVE-2026-34486 to the KEV catalog on 4 August 2026, which by definition means CISA has reliable evidence of active exploitation. The Hacker News reported the same day that the exploitation is attributed to an autonomous AI-enabled campaign that Palo Alto Networks Unit 42 links to a Chinese-speaking actor operating as knaithe and KnYuan, which used the DeepSeek model through the Hermes agent framework to scan and attack internet-exposed devices, targeting over 460 hosts across a mix of automated and manual techniques. US federal civilian agencies were given until 7 August 2026 to remediate under the KEV timeline.

The through-line for a defender is that the window for treating this as theoretical closed on 4 August. If your receiver is reachable, you are now inside an actively exploited exposure.

India-specific considerations

For teams running Java platforms in India, two points sharpen the priority. Under the Digital Personal Data Protection Act 2023, an unauthorised access to session data replicated across a cluster is a personal data breach if those sessions carry personal data, which most authenticated web sessions do, and that triggers notification obligations you can only assess if you have retained the receiver logs. Keep the Failed to decrypt message events and inbound connection logs on port 4000; they are the evidence of whether the sink was hit.

The cost context is not abstract either. IBM's 2026 Cost of a Data Breach Report, released 3 August 2026, puts the average Indian breach at ₹25.5 crore, up 15.9% year on year, against a global average of $4.99 million, and it names offensive security testing as India's single largest cost-reducing factor, saving an average of ₹2.47 crore. A deserialization sink on a clustering port is exactly the kind of finding a penetration test surfaces before an attacker does.

For the wider pattern of treating emergency patches as the start rather than the end of the work, our Node.js July 2026 patch response playbook and our approach to triaging a large Oracle Critical Patch Update by risk apply the same discipline to other stacks. This piece sits in our web and API engineering cluster under the Interop 2026 web platform developer guide.

What to change so the next one is cheaper

Three habits that would have blunted this specific bug.

Treat any security patch to a component you rely on for a trust boundary as a change that needs its own verification, not a routine bump. The fix for CVE-2026-29146 was itself a security fix, and it opened CVE-2026-34486. A quick post-upgrade check that the encryption gate still drops undecryptable traffic would have caught the regression.

Assume every internal listening port is reachable by an attacker who has a foothold somewhere in your network, and segment accordingly. The Kubernetes namespace trap is the clearest example, but it applies to any flat network.

Put a JVM deserialization filter in place before you need it. It is the one control here that would have stopped exploitation even on a fully vulnerable, fully exposed node, because it operates at the sink rather than the perimeter. The perimeter is where you hope the attacker is not; the sink is where the code actually runs.

The real cost of this class of bug is rarely the upgrade. It is the deserialization sink you left unfiltered because the encryption in front of it was supposed to be enough.

FAQ

How eCorpIT can help

eCorpIT is a senior-led engineering organisation in Gurugram, ISO 27001:2022 certified and assessed at CMMI Level 5, and we do Java platform hardening and application portfolio analysis for teams running Tomcat, Spring and legacy clustered deployments. For this issue that means auditing your interceptor configuration and receiver exposure, applying the patch across environments, and putting a tested deserialization filter and network segmentation in place so the sink is closed for good. We also run API integration and modernization work on the same stacks, and design applications aligned with DPDP Act 2023 requirements. If you run Tomcat clustering and are not sure whether you are exposed, talk to us at /contact-us/.

References

  1. Fail Open, Game Over: Turning a One-Line Tomcat Fix into Unauthenticated RCE - Striga
  1. CVE-2026-34486 record - CVE Program
  1. CVE-2026-29146 record - CVE Program
  1. CVE-2026-34486: Apache Tomcat Tribes Regression Creates Unauthenticated RCE Path - SOCRadar
  1. Apache Tomcat 11 Security Advisories - Apache Software Foundation
  1. CISA Adds Three Known Exploited Vulnerabilities to Catalog, 4 August 2026 - CISA
  1. Known Exploited Vulnerabilities Catalog - CISA
  1. CISA Flags Langflow RCE, Tomcat, and N-central Flaws as Actively Exploited - The Hacker News, 5 August 2026
  1. The regression commit on the 11.0.x branch - Apache Tomcat, GitHub
  1. CWE-502: Deserialization of Untrusted Data - MITRE
  1. CWE-636: Not Failing Securely (Fail Open) - MITRE
  1. India Records Its Highest Average Cost of a Data Breach at INR 255 Million in 2026 - IBM, 3 August 2026

Last updated: 5 August 2026.

Frequently asked

Quick answers.

01 Am I affected if I run Tomcat but do not use clustering?
No. CVE-2026-34486 only affects deployments that enable Tribes clustering with EncryptInterceptor configured on an affected build. If grep -rn "EncryptInterceptor" $CATALINA_HOME/conf/ returns nothing, the vulnerable receive path is not in your interceptor chain and this CVE does not apply, even on version 11.0.20, 10.1.53 or 9.0.116.
02 Which exact versions do I need to move off?
The affected builds are 11.0.20, 10.1.53 and 9.0.116. Upgrade to 11.0.21, 10.1.54 or 9.0.117 respectively, all released between 2 and 4 April 2026. Builds earlier than the affected ones, such as 11.0.18, never had the regression, and Tomcat 8.5.x is not affected because it has no EncryptInterceptor component.
03 Does patching fully fix the problem?
Patching restores fail-closed behaviour and closes this specific regression. It does not filter the underlying ObjectInputStream.readObject() sink, which still runs on decrypted messages. Add a JVM-wide jdk.serialFilter, remove unused gadget libraries, and restrict the receiver port, so a future bug in the same path does not reach code execution.
04 Why does the encryption not protect me?
The encryption is the component that failed. The regression made EncryptInterceptor forward messages that fail decryption instead of dropping them, so undecryptable attacker bytes reach the deserialization routine anyway. The encryption layer became a gate that could be bypassed rather than a hard stop, which is why configured encryption offers no protection on the affected versions.
05 What does exploitation look like in my logs?
A single SEVERE entry reading "Failed to decrypt message" from EncryptInterceptor.messageReceived, typically with an AEADBadTagException or BadPaddingException. No deserialization error follows, because the gadget runs silently. Correlate those entries with unexpected inbound TCP connections to the receiver port, 4000 by default, to spot attempts.
06 Is CVE-2026-34486 being exploited in the wild?
Yes, as of 4 August 2026. CISA added it to the Known Exploited Vulnerabilities catalog that day, and The Hacker News reported exploitation attributed to an autonomous AI-driven campaign that Palo Alto Networks Unit 42 links to an actor operating as knaithe. When SOCRadar assessed it in April 2026 there was no confirmed exploitation, so the status changed over time.
07 What is the fastest interim control if I cannot patch today?
Restrict network access to the receiver port. Bind the Receiver to a private cluster-only interface and firewall TCP 4000 to peer nodes only. In Kubernetes, apply a default-deny NetworkPolicy that permits port 4000 solely between the cluster's own pods, which removes the same-namespace reachability that otherwise exposes every node.
08 Does a modern JDK protect against the gadget chain?
Not by itself. JDK 8u72 broke the older CommonsCollections1 and CC3 chains, but the CommonsCollections6 chain uses HashSet as its entry point and works on JDK 17 and 21. A current runtime does not remove the deserialization risk; a configured jdk.serialFilter and a clean classpath do.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.