VYPR
Moderate severityNVD Advisory· Published Oct 10, 2023· Updated Oct 29, 2025

Apache Tomcat: Trailer header parsing too lenient

CVE-2023-45648

Description

Improper Input Validation vulnerability in Apache Tomcat.Tomcat from 11.0.0-M1 through 11.0.0-M11, from 10.1.0-M1 through 10.1.13, from 9.0.0-M1 through 9.0.81 and from 8.5.0 through 8.5.93 did not correctly parse HTTP trailer headers. A specially crafted, invalid trailer header could cause Tomcat to treat a single request as multiple requests leading to the possibility of request smuggling when behind a reverse proxy.

Older, EOL versions may also be affected.

Users are recommended to upgrade to version 11.0.0-M12 onwards, 10.1.14 onwards, 9.0.81 onwards or 8.5.94 onwards, which fix the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Improper validation of HTTP trailer headers in Apache Tomcat allows request smuggling when behind a reverse proxy, affecting multiple versions.

Vulnerability

Details

CVE-2023-45648 is an improper input validation vulnerability in Apache Tomcat's handling of HTTP trailer headers. The flaw exists in the parseHeader() methods of Http11InputBuffer and ChunkedInputFilter, which did not correctly validate trailer headers [1][2][3][4]. A specially crafted, invalid trailer header can cause Tomcat to misinterpret a single HTTP request as multiple requests, leading to request smuggling [1].

Exploitation

An attacker can exploit this issue by sending a malicious HTTP request with a crafted trailer header to a Tomcat instance that is behind a reverse proxy. No authentication is required to trigger the vulnerability. The improper parsing logic, fixed by inverting a condition check (moving the check for chr != Constants.HT after isControl()), allows control characters other than horizontal tab to be interpreted incorrectly, enabling request splitting [2][3][4].

Impact

Successful exploitation could allow an attacker to perform HTTP request smuggling attacks. This can lead to security bypass, cache poisoning, credential theft, and session hijacking, depending on the proxy configuration and application logic [1].

Mitigation

Users should upgrade to Apache Tomcat versions 11.0.0-M12, 10.1.14, 9.0.81, or 8.5.94, which contain the fix. Older, end-of-life versions may also be affected and should be upgraded or decommissioned [1].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apache.tomcat:tomcatMaven
>= 11.0.0-M1, < 11.0.0-M1211.0.0-M12
org.apache.tomcat:tomcatMaven
>= 10.1.0-M1, < 10.1.1410.1.14
org.apache.tomcat:tomcatMaven
>= 9.0.0-M1, < 9.0.819.0.81
org.apache.tomcat:tomcatMaven
>= 8.5.0, < 8.5.948.5.94
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 11.0.0-M1, < 11.0.0-M1211.0.0-M12
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 10.1.0-M1, < 10.1.1410.1.14
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 9.0.0-M1, < 9.0.819.0.81
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 8.5.0, < 8.5.948.5.94

Affected products

32

Patches

4
c83fe47725f7

Align processing of trailer headers with standard processing

https://github.com/apache/tomcatMark ThomasOct 5, 2023via ghsa
4 files changed · +26 2
  • java/org/apache/coyote/http11/filters/ChunkedInputFilter.java+14 1 modified
    @@ -31,6 +31,7 @@
     import org.apache.tomcat.util.buf.HexUtils;
     import org.apache.tomcat.util.buf.MessageBytes;
     import org.apache.tomcat.util.http.MimeHeaders;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.res.StringManager;
     
    @@ -503,6 +504,13 @@ protected void parseEndChunk() throws IOException {
     
         private boolean parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * Http11InputBuffer.parseHeader(). Why not use a common implementation? In short, this code uses blocking
    +         * reads whereas Http11InputBuffer using non-blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
    +
             MimeHeaders headers = request.getMimeHeaders();
     
             byte chr = 0;
    @@ -549,6 +557,9 @@ private boolean parseHeader() throws IOException {
     
                 if (chr == Constants.COLON) {
                     colon = true;
    +            } else if (!HttpParser.isToken(chr)) {
    +                // Non-token characters are illegal in header names
    +                throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderName"));
                 } else {
                     trailingHeaders.append(chr);
                 }
    @@ -610,7 +621,9 @@ private boolean parseHeader() throws IOException {
                     if (chr == Constants.CR || chr == Constants.LF) {
                         parseCRLF(true);
                         eol = true;
    -                } else if (chr == Constants.SP) {
    +                } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
    +                    throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderValue"));
    +                } else if (chr == Constants.SP || chr == Constants.HT) {
                         trailingHeaders.append(chr);
                     } else {
                         trailingHeaders.append(chr);
    
  • java/org/apache/coyote/http11/filters/LocalStrings.properties+2 0 modified
    @@ -24,6 +24,8 @@ chunkedInputFilter.invalidCrlfCRCR=Invalid end of line sequence (CRCR)
     chunkedInputFilter.invalidCrlfNoCR=Invalid end of line sequence (No CR before LF)
     chunkedInputFilter.invalidCrlfNoData=Invalid end of line sequence (no data available to read)
     chunkedInputFilter.invalidHeader=Invalid chunk header
    +chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-token character in name)
    +chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value)
     chunkedInputFilter.maxExtension=maxExtensionSize exceeded
     chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
     
    
  • java/org/apache/coyote/http11/Http11InputBuffer.java+7 1 modified
    @@ -830,6 +830,12 @@ private boolean fill(boolean block) throws IOException {
          */
         private HeaderParseStatus parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * ChunkedInputFilter.parseHeader(). Why not use a common implementation? In short, this code uses non-blocking
    +         * reads whereas ChunkedInputFilter using blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
             while (headerParsePos == HeaderParsePosition.HEADER_START) {
     
                 // Read new bytes if needed
    @@ -972,7 +978,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                         } else if (prevChr == Constants.CR) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
    -                    } else if (chr != Constants.HT && HttpParser.isControl(chr)) {
    +                    } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
                         } else if (chr == Constants.SP || chr == Constants.HT) {
    
  • webapps/docs/changelog.xml+3 0 modified
    @@ -156,6 +156,9 @@
           <fix>
             Avoid rare thread safety issue accessing message digest map. (remm)
           </fix>
    +      <fix>
    +        Align validation of HTTP trailer fields with standard fields. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
8ecff306507b

Align processing of trailer headers with standard processing

https://github.com/apache/tomcatMark ThomasOct 5, 2023via ghsa
4 files changed · +26 2
  • java/org/apache/coyote/http11/filters/ChunkedInputFilter.java+14 1 modified
    @@ -30,6 +30,7 @@
     import org.apache.coyote.http11.InputFilter;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.HexUtils;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.res.StringManager;
     
    @@ -443,6 +444,13 @@ protected void parseEndChunk() throws IOException {
     
         private boolean parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * Http11InputBuffer.parseHeader(). Why not use a common implementation? In short, this code uses blocking
    +         * reads whereas Http11InputBuffer using non-blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
    +
             Map<String,String> headers = request.getTrailerFields();
     
             byte chr = 0;
    @@ -489,6 +497,9 @@ private boolean parseHeader() throws IOException {
     
                 if (chr == Constants.COLON) {
                     colon = true;
    +            } else if (!HttpParser.isToken(chr)) {
    +                // Non-token characters are illegal in header names
    +                throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderName"));
                 } else {
                     trailingHeaders.append(chr);
                 }
    @@ -550,7 +561,9 @@ private boolean parseHeader() throws IOException {
                     if (chr == Constants.CR || chr == Constants.LF) {
                         parseCRLF(true);
                         eol = true;
    -                } else if (chr == Constants.SP) {
    +                } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
    +                    throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderValue"));
    +                } else if (chr == Constants.SP || chr == Constants.HT) {
                         trailingHeaders.append(chr);
                     } else {
                         trailingHeaders.append(chr);
    
  • java/org/apache/coyote/http11/filters/LocalStrings.properties+2 0 modified
    @@ -24,6 +24,8 @@ chunkedInputFilter.invalidCrlfCRCR=Invalid end of line sequence (CRCR)
     chunkedInputFilter.invalidCrlfNoCR=Invalid end of line sequence (No CR before LF)
     chunkedInputFilter.invalidCrlfNoData=Invalid end of line sequence (no data available to read)
     chunkedInputFilter.invalidHeader=Invalid chunk header
    +chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-token character in name)
    +chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value)
     chunkedInputFilter.maxExtension=maxExtensionSize exceeded
     chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
     
    
  • java/org/apache/coyote/http11/Http11InputBuffer.java+7 1 modified
    @@ -830,6 +830,12 @@ private boolean fill(boolean block) throws IOException {
          */
         private HeaderParseStatus parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * ChunkedInputFilter.parseHeader(). Why not use a common implementation? In short, this code uses non-blocking
    +         * reads whereas ChunkedInputFilter using blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
             while (headerParsePos == HeaderParsePosition.HEADER_START) {
     
                 // Read new bytes if needed
    @@ -972,7 +978,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                         } else if (prevChr == Constants.CR) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
    -                    } else if (chr != Constants.HT && HttpParser.isControl(chr)) {
    +                    } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
                         } else if (chr == Constants.SP || chr == Constants.HT) {
    
  • webapps/docs/changelog.xml+3 0 modified
    @@ -160,6 +160,9 @@
             Improve statistics collection for upgraded connections under load.
             (remm)
           </fix>
    +      <fix>
    +        Align validation of HTTP trailer fields with standard fields. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
eb5c094e5560

Align processing of trailer headers with standard processing

https://github.com/apache/tomcatMark ThomasOct 5, 2023via ghsa
4 files changed · +26 2
  • java/org/apache/coyote/http11/filters/ChunkedInputFilter.java+14 1 modified
    @@ -30,6 +30,7 @@
     import org.apache.coyote.http11.InputFilter;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.HexUtils;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.res.StringManager;
     
    @@ -443,6 +444,13 @@ protected void parseEndChunk() throws IOException {
     
         private boolean parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * Http11InputBuffer.parseHeader(). Why not use a common implementation? In short, this code uses blocking
    +         * reads whereas Http11InputBuffer using non-blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
    +
             Map<String,String> headers = request.getTrailerFields();
     
             byte chr = 0;
    @@ -489,6 +497,9 @@ private boolean parseHeader() throws IOException {
     
                 if (chr == Constants.COLON) {
                     colon = true;
    +            } else if (!HttpParser.isToken(chr)) {
    +                // Non-token characters are illegal in header names
    +                throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderName"));
                 } else {
                     trailingHeaders.append(chr);
                 }
    @@ -550,7 +561,9 @@ private boolean parseHeader() throws IOException {
                     if (chr == Constants.CR || chr == Constants.LF) {
                         parseCRLF(true);
                         eol = true;
    -                } else if (chr == Constants.SP) {
    +                } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
    +                    throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderValue"));
    +                } else if (chr == Constants.SP || chr == Constants.HT) {
                         trailingHeaders.append(chr);
                     } else {
                         trailingHeaders.append(chr);
    
  • java/org/apache/coyote/http11/filters/LocalStrings.properties+2 0 modified
    @@ -24,6 +24,8 @@ chunkedInputFilter.invalidCrlfCRCR=Invalid end of line sequence (CRCR)
     chunkedInputFilter.invalidCrlfNoCR=Invalid end of line sequence (No CR before LF)
     chunkedInputFilter.invalidCrlfNoData=Invalid end of line sequence (no data available to read)
     chunkedInputFilter.invalidHeader=Invalid chunk header
    +chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-token character in name)
    +chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value)
     chunkedInputFilter.maxExtension=maxExtensionSize exceeded
     chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
     
    
  • java/org/apache/coyote/http11/Http11InputBuffer.java+7 1 modified
    @@ -826,6 +826,12 @@ private boolean fill(boolean block) throws IOException {
          */
         private HeaderParseStatus parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * ChunkedInputFilter.parseHeader(). Why not use a common implementation? In short, this code uses non-blocking
    +         * reads whereas ChunkedInputFilter using blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
             while (headerParsePos == HeaderParsePosition.HEADER_START) {
     
                 // Read new bytes if needed
    @@ -968,7 +974,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                         } else if (prevChr == Constants.CR) {
                             // Invalid value - also need to delete header
                             return skipLine();
    -                    } else if (chr != Constants.HT && HttpParser.isControl(chr)) {
    +                    } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
                             // Invalid value - also need to delete header
                             return skipLine();
                         } else if (chr == Constants.SP || chr == Constants.HT) {
    
  • webapps/docs/changelog.xml+3 0 modified
    @@ -170,6 +170,9 @@
             <code>newPushBuilder()</code> will always return <code>null</code>.
             (markt)
           </update>
    +      <fix>
    +        Align validation of HTTP trailer fields with standard fields. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
59583245639d

Align processing of trailer headers with standard processing

https://github.com/apache/tomcatMark ThomasOct 5, 2023via ghsa
4 files changed · +26 2
  • java/org/apache/coyote/http11/filters/ChunkedInputFilter.java+14 1 modified
    @@ -30,6 +30,7 @@
     import org.apache.coyote.http11.InputFilter;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.HexUtils;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.res.StringManager;
     
    @@ -443,6 +444,13 @@ protected void parseEndChunk() throws IOException {
     
         private boolean parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * Http11InputBuffer.parseHeader(). Why not use a common implementation? In short, this code uses blocking
    +         * reads whereas Http11InputBuffer using non-blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
    +
             Map<String,String> headers = request.getTrailerFields();
     
             byte chr = 0;
    @@ -489,6 +497,9 @@ private boolean parseHeader() throws IOException {
     
                 if (chr == Constants.COLON) {
                     colon = true;
    +            } else if (!HttpParser.isToken(chr)) {
    +                // Non-token characters are illegal in header names
    +                throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderName"));
                 } else {
                     trailingHeaders.append(chr);
                 }
    @@ -550,7 +561,9 @@ private boolean parseHeader() throws IOException {
                     if (chr == Constants.CR || chr == Constants.LF) {
                         parseCRLF(true);
                         eol = true;
    -                } else if (chr == Constants.SP) {
    +                } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
    +                    throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderValue"));
    +                } else if (chr == Constants.SP || chr == Constants.HT) {
                         trailingHeaders.append(chr);
                     } else {
                         trailingHeaders.append(chr);
    
  • java/org/apache/coyote/http11/filters/LocalStrings.properties+2 0 modified
    @@ -24,6 +24,8 @@ chunkedInputFilter.invalidCrlfCRCR=Invalid end of line sequence (CRCR)
     chunkedInputFilter.invalidCrlfNoCR=Invalid end of line sequence (No CR before LF)
     chunkedInputFilter.invalidCrlfNoData=Invalid end of line sequence (no data available to read)
     chunkedInputFilter.invalidHeader=Invalid chunk header
    +chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-token character in name)
    +chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value)
     chunkedInputFilter.maxExtension=maxExtensionSize exceeded
     chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
     
    
  • java/org/apache/coyote/http11/Http11InputBuffer.java+7 1 modified
    @@ -830,6 +830,12 @@ private boolean fill(boolean block) throws IOException {
          */
         private HeaderParseStatus parseHeader() throws IOException {
     
    +        /*
    +         * Implementation note: Any changes to this method probably need to be echoed in
    +         * ChunkedInputFilter.parseHeader(). Why not use a common implementation? In short, this code uses non-blocking
    +         * reads whereas ChunkedInputFilter using blocking reads. The code is just different enough that a common
    +         * implementation wasn't viewed as practical.
    +         */
             while (headerParsePos == HeaderParsePosition.HEADER_START) {
     
                 // Read new bytes if needed
    @@ -972,7 +978,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                         } else if (prevChr == Constants.CR) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
    -                    } else if (chr != Constants.HT && HttpParser.isControl(chr)) {
    +                    } else if (HttpParser.isControl(chr) && chr != Constants.HT) {
                             // Invalid value - also need to delete header
                             return skipLine(true);
                         } else if (chr == Constants.SP || chr == Constants.HT) {
    
  • webapps/docs/changelog.xml+3 0 modified
    @@ -160,6 +160,9 @@
             Improve statistics collection for upgraded connections under load.
             (remm)
           </fix>
    +      <fix>
    +        Align validation of HTTP trailer fields with standard fields. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.