CVE-2026-86350

HTTP Request Smuggling
Affects
Apache Tomcat
in
Apache Tomcat
No items found.
Versions
>=8.5.59 <=8.5.100, >=9.0.118 <9.0.122, >=10.1.55 <10.1.60, >=11.0.22 <11.0.26

Patch Available.

Exclamation circle icon
Patch Available

This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.

Overview

Apache Tomcat is an open-source implementation of the Jakarta Servlet, Jakarta Pages, Jakarta Expression Language, Jakarta WebSocket, Jakarta Annotations, and Jakarta Authentication specifications. It serves Java web applications either as a standalone servlet container and HTTP server or embedded inside an application through the tomcat-embed-core artifact. Its Catalina servlet container (org.apache.tomcat:tomcat-catalina) supplies the request-processing pipeline, including Valves such as RewriteValve, along with connectors, realms, and session management.

An HTTP Request Smuggling vulnerability (CVE-2026-86350) has been identified in the HTTP/2 HPACK decoder, which allows attackers to send a header block containing an invalid header field name or value so that decoding is aborted part way through, leaving the connection-scoped HPACK state inconsistent and allowing request headers to be attributed to the wrong stream.

Per OWASP: "the attacker exploits the fact that some specially crafted HTTP messages can be parsed and interpreted in different ways depending on the agent that receives them. HTTP smuggling requires some level of knowledge about the different agents that are handling the HTTP messages (web server, proxy, firewall)".

This issue affects HTTP/2 request header processing of Apache Tomcat.

Details

Module Info

  • Product: Apache Tomcat
  • Affected packages: org.apache.tomcat:tomcat-coyote, org.apache.tomcat.embed:tomcat-embed-core
  • Affected versions: >=9.0.118 <9.0.122, >=10.1.55 <10.1.60, >=11.0.22 <11.0.26
  • Package manager: Maven

Vulnerability Info

This High-severity vulnerability is found in the org.apache.tomcat:tomcat-coyote package in the HTTP/2 implementation of Apache Tomcat.

The remediation for CVE-2026-41293 added HTTP/2 header field validation to the HPACK decoder itself. HpackDecoder.readHpackString and HPackHuffman.decode gained an isFieldName flag and rejected disallowed characters inline by throwing IllegalArgumentException from the middle of the decode loop:

private String readHpackString(ByteBuffer buffer, boolean isFieldName) throws HpackException {
    ...
    boolean huffman = (data & 0b10000000) != 0;
    if (huffman) {
        return readHuffmanString(length, buffer, isFieldName);
    }
    StringBuilder stringBuilder = new StringBuilder(length);
    for (int i = 0; i < length; ++i) {
        char c = (char) (buffer.get() & 0xFF);
        if (isFieldName) {
            if (HttpParser.isToken(c) && !Character.isUpperCase(c)) {
                stringBuilder.append(c);
            } else {
                throw new IllegalArgumentException(
                        sm.getString("hpackdecoder.illegalCharacterName", Character.toString(c)));
            }
        } else {
            if ((i == 0 || i == length - 1) && HttpParser.isFieldVChar(c) ||
                    i > 0 && i < length - 1 && HttpParser.isFieldContent(c)) {
                stringBuilder.append(c);
            } else {
                throw new IllegalArgumentException(
                        sm.getString("hpackdecoder.illegalCharacterValue", Character.toString(c)));
            }
        }
    }
    return stringBuilder.toString();
}

The Huffman path carried the same inline checks, including an end-of-value check performed after the decoding loop had already run:

public static void decode(ByteBuffer data, int length, StringBuilder target, boolean isFieldName)
        throws HpackException {
    ...
            c = (char) (val & LOW_MASK);
            if (isFieldName) {
                if (!HttpParser.isToken(c) || Character.isUpperCase(c)) {
                    throw new IllegalArgumentException(sm
                            .getString("hpackhuffman.decode.illegalCharacterName", Character.toString(c)));
                }
            } else {
                if (firstChar) {
                    if (!HttpParser.isFieldVChar(c)) {
                        throw new IllegalArgumentException(sm.getString(
                                "hpackhuffman.decode.illegalCharacterValue.start", Character.toString(c)));
                    }
                    firstChar = false;
                } else {
                    if (!HttpParser.isFieldContent(c)) {
                        throw new IllegalArgumentException(sm.getString(
                                "hpackhuffman.decode.illegalCharacterValue", Character.toString(c)));
                    }
                }
            }
            target.append(c);
    ...
    if (!isFieldName && !HttpParser.isFieldVChar(c)) {
        throw new IllegalArgumentException(
                sm.getString("hpackhuffman.decode.illegalCharacterValue.end", Character.toString(c)));
    }
}

Http2Parser translated that exception into a per-stream protocol error:

try {
    hpackDecoder.decode(headerReadBuffer);
} catch (HpackException hpe) {
    throw new ConnectionException(sm.getString("http2Parser.processFrameHeaders.decodingFailed"),
            Http2Error.COMPRESSION_ERROR, hpe);
} catch (IllegalArgumentException iae) {
    throw new StreamException("Invalid headers", Http2Error.PROTOCOL_ERROR, streamId, iae);
}

The HPACK decoder is connection-scoped and stateful: it maintains the dynamic header table shared by every stream on the connection, and it has no access to the per-stream validation state that Stream manages. Throwing out of the middle of the decode loop therefore abandons the header block while the decoder is still positioned inside it, so the dynamic table and the remaining header block bytes are left in an inconsistent state relative to the streams still in flight. A remote client that opens concurrent streams and sends a HEADERS frame containing a header field name or value with a character that fails these checks, for example an upper-case byte in a field name, can use that desynchronization to have headers from one request associated with a different request on the same connection. Because header values commonly carry authorization, host, and routing data, a mix-up across streams can expose one client's headers to another client's request or defeat security decisions that depend on those headers.

The validation itself is required behavior, so the remediation keeps it but moves both the field name loop and the field value loop into Stream.emitHeader, which owns the per-stream validation state and can record a StreamException with Http2Error.PROTOCOL_ERROR for that one stream instead of unwinding the shared decoder.

Mitigation

Only recent versions of Apache Tomcat are community-supported. The community support version will not receive any updates to address this issue. For more information, see here. Apache Tomcat 8.5 is an unsupported line upstream and there is no publicly available fix for it; NES for Apache Tomcat is the remedy for that line.

Users of the affected components should apply one of the following mitigations:

  • Upgrade to a patched version of Apache Tomcat.
  • Leverage a commercial support partner like HeroDevs for post-EOL security support.

Credits

  • Jeppe Weikop
Vulnerability Details
Severity
Level
CVSS Assessment
Low
>=0 <4
Medium
>=4 <6
High
>=6 <8
Critical
>=8 <10
High
ID
CVE-2026-86350
PROJECT Affected
Apache Tomcat
Versions Affected
>=8.5.59 <=8.5.100, >=9.0.118 <9.0.122, >=10.1.55 <10.1.60, >=11.0.22 <11.0.26
NES Versions Affected
Published date
September 23, 2026
≈ Fix date
September 25, 2026
Category
HTTP Request Smuggling
Vex Document
Download VEXHow do I use it?
Sign up for the latest vulnerability alerts fixed in
NES for Apache Tomcat
Rss feed icon
Subscribe via RSS
or

By submitting the form I acknowledge receipt of our Privacy Policy.

Thanks for signing up for our Newsletter! We look forward to connecting with you.
Oops! Something went wrong while submitting the form.