VYPR
Moderate severityNVD Advisory· Published Nov 18, 2024· Updated Jan 24, 2025

Apache Tomcat: Request/response mix-up with HTTP/2

CVE-2024-52317

Description

Incorrect object re-cycling and re-use vulnerability in Apache Tomcat. Incorrect recycling of the request and response used by HTTP/2 requests could lead to request and/or response mix-up between users.

This issue affects Apache Tomcat: from 11.0.0-M23 through 11.0.0-M26, from 10.1.27 through 10.1.30, from 9.0.92 through 9.0.95.

Users are recommended to upgrade to version 11.0.0, 10.1.31 or 9.0.96, which fixes the issue.

AI Insight

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

Apache Tomcat HTTP/2 stream object recycling flaw allows request/response mix-up between users.

Root

Cause

CVE-2024-52317 is an incorrect object recycling vulnerability in Apache Tomcat's HTTP/2 stream handling [1]. The Stream.recycle() method, despite its name, did not fully recycle Stream objects for reuse; instead, it called the handler to replace the stream with a lighter-weight RecycledStream [2][3][4]. However, the underlying Stream object was retained in memory for a period after the stream closed, and the recycling process did not properly reset all state, leading to the potential for a subsequent HTTP/2 request to be associated with a previously used Stream object [2][3][4].

Attack

Vector

An attacker can exploit this bug by sending specially crafted HTTP/2 requests to a vulnerable Tomcat server [1]. The attack does not require authentication and can be performed over the network. The core issue is that when a stream is closed, the close() method called recycle(), which performed a partial replacement but left the Stream object in a state where it could be re-associated with a new request [2][3][4]. The fix introduces a recycled flag and a lock, and it splits the method into a replace() (for memory optimization) and a separate recycle() that properly marks the stream as available [2][3][4].

Impact

Successful exploitation can result in a request and/or response mix-up between different users [1]. This means an attacker could potentially read responses intended for other users or inject their own data into another user's response, leading to information disclosure or data corruption. The vulnerability affects Tomcat versions 11.0.0-M23 through 11.0.0-M26, 10.1.27 through 10.1.30, and 9.0.92 through 9.0.95 [1].

Mitigation

Users are recommended to upgrade to Tomcat version 11.0.0, 10.1.31, or 9.0.96, which contain the fix [1]. No workarounds are mentioned in the advisory [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.embed:tomcat-embed-coreMaven
>= 9.0.92, < 9.0.969.0.96
org.apache.tomcat:tomcat-coyoteMaven
>= 9.0.92, < 9.0.969.0.96
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 10.1.27, < 10.1.3110.1.31
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 11.0.0-M23, < 11.0.011.0.0
org.apache.tomcat:tomcat-coyoteMaven
>= 10.1.27, < 10.1.3110.1.31
org.apache.tomcat:tomcat-coyoteMaven
>= 11.0.0-M23, < 11.0.011.0.0

Affected products

27

Patches

3
146f94f87ea3

Split Stream.recycle() into replace() and recycle()

https://github.com/apache/tomcatMark ThomasOct 1, 2024via ghsa
4 files changed · +55 15
  • java/org/apache/coyote/http2/LocalStrings.properties+2 1 modified
    @@ -109,7 +109,8 @@ stream.inputBuffer.signal=Data added to inBuffer when read thread is waiting. Si
     stream.inputBuffer.swallowUnread=Swallowing [{0}] bytes previously read into input stream buffer
     stream.notWritable=Connection [{0}], Stream [{1}], This stream is not writable
     stream.outputBuffer.flush.debug=Connection [{0}], Stream [{1}], flushing output with buffer at position [{2}], writeInProgress [{3}] and closed [{4}]
    -stream.recycle=Connection [{0}], Stream [{1}] has been recycled
    +stream.recycle.duplicate=Connection [{0}], Stream [{1}] Duplicate request to recycle the associated request and response has been ignored
    +stream.recycle.first=Connection [{0}], Stream [{1}] The associated request and response have been recycled
     stream.reset.fail=Connection [{0}], Stream [{1}], Failed to reset stream
     stream.reset.receive=Connection [{0}], Stream [{1}], Reset received due to [{2}]
     stream.reset.send=Connection [{0}], Stream [{1}], Reset sent due to [{2}]
    
  • java/org/apache/coyote/http2/Stream.java+41 9 modified
    @@ -109,6 +109,9 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter {
         private volatile int urgency = Priority.DEFAULT_URGENCY;
         private volatile boolean incremental = Priority.DEFAULT_INCREMENTAL;
     
    +    private final Object recycledLock = new Object();
    +    private volatile boolean recycled = false;
    +
     
         Stream(Integer identifier, Http2UpgradeHandler handler) {
             this(identifier, handler, null);
    @@ -784,20 +787,15 @@ final void close(Http2Exception http2Exception) {
             } else {
                 handler.closeConnection(http2Exception);
             }
    -        recycle();
    +        replace();
         }
     
     
         /*
    -     * This method is called recycle for consistency with the rest of the Tomcat code base. Currently, it calls the
    -     * handler to replace this stream with an implementation that uses less memory. It does not fully recycle the Stream
    -     * ready for re-use since Stream objects are not re-used. This is useful because Stream instances are retained for a
    -     * period after the Stream closes.
    +     * This method calls the handler to replace this stream with an implementation that uses less memory. This is useful
    +     * because Stream instances are retained for a period after the Stream closes.
          */
    -    final void recycle() {
    -        if (log.isTraceEnabled()) {
    -            log.trace(sm.getString("stream.recycle", getConnectionId(), getIdAsString()));
    -        }
    +    final void replace() {
             int remaining;
             // May be null if stream was closed before any DATA frames were processed.
             ByteBuffer inputByteBuffer = getInputByteBuffer(false);
    @@ -807,6 +805,40 @@ final void recycle() {
                 remaining = inputByteBuffer.remaining();
             }
             handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state, remaining));
    +    }
    +
    +
    +    /*
    +     * This method is called recycle for consistency with the rest of the Tomcat code base. It does not recycle the
    +     * Stream since Stream objects are not re-used. It does recycle the request and response objects and ensures that
    +     * this is only done once.
    +     *
    +     * replace() should have been called before calling this method.
    +     *
    +     * It is important that this method is not called until any concurrent processing for the stream has completed. This
    +     * is currently achieved by:
    +     * - only the StreamProcessor calls this method
    +     * - the Http2UpgradeHandler does not call this method
    +     * - this method is called once the StreamProcessor considers the Stream closed
    +     *
    +     * In theory, the protection against duplicate calls is not required in this method (the code in StreamProcessor
    +     * should be sufficient) but it is implemented as precaution along with the WARN level logging.
    +     */
    +    final void recycle() {
    +        if (recycled) {
    +            log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +            return;
    +        }
    +        synchronized (recycledLock) {
    +            if (recycled) {
    +                log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +                return;
    +            }
    +            recycled = true;
    +        }
    +        if (log.isTraceEnabled()) {
    +            log.trace(sm.getString("stream.recycle.first", getConnectionId(), getIdAsString()));
    +        }
             coyoteRequest.recycle();
             coyoteResponse.recycle();
             handler.getProtocol().pushRequestAndResponse(coyoteRequest);
    
  • java/org/apache/coyote/http2/StreamProcessor.java+6 5 modified
    @@ -87,9 +87,9 @@ final void process(SocketEvent event) {
                 try {
                     /*
                      * In some scenarios, error handling may trigger multiple ERROR events for the same stream. The first
    -                 * ERROR event process will close the stream and recycle it. Once the stream has been recycled it should
    -                 * not be used for processing any further events. The check below ensures that this is the case. In
    -                 * particular, Stream.recycle() should not be called more than once per Stream.
    +                 * ERROR event processed will close the stream, replace it and recycle it. Once the stream has been
    +                 * replaced it should not be used for processing any further events. When it is known that processing is
    +                 * going to be a NO-OP, exit early.
                      */
                     if (!stream.equals(handler.getStream(stream.getIdAsInt()))) {
                         return;
    @@ -130,8 +130,8 @@ final void process(SocketEvent event) {
                                 stream.close(se);
                             } else {
                                 if (!stream.isActive()) {
    -                                // stream.close() will call recycle so only need it here
    -                                stream.recycle();
    +                                // Close calls replace() so need the same call here
    +                                stream.replace();
                                 }
                             }
                         }
    @@ -146,6 +146,7 @@ final void process(SocketEvent event) {
                         state = SocketState.CLOSED;
                     } finally {
                         if (state == SocketState.CLOSED) {
    +                        stream.recycle();
                             recycle();
                         }
                     }
    
  • webapps/docs/changelog.xml+6 0 modified
    @@ -173,6 +173,12 @@
             stream nor are trailer fields for an in progress stream swallowed if the
             Connector is paused before the trailer fields are received. (markt)
           </fix>
    +      <fix>
    +        Ensure the request and response are not recycled too soon for an HTTP/2
    +        stream when a stream level error is detected during the processing of
    +        incoming HTTP/2 frames. This could lead to incorrect processing times
    +        appearing in the access log. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
9e840ccacb40

Split Stream.recycle() into replace() and recycle()

https://github.com/apache/tomcatMark ThomasOct 1, 2024via ghsa
4 files changed · +55 15
  • java/org/apache/coyote/http2/LocalStrings.properties+2 1 modified
    @@ -109,7 +109,8 @@ stream.inputBuffer.signal=Data added to inBuffer when read thread is waiting. Si
     stream.inputBuffer.swallowUnread=Swallowing [{0}] bytes previously read into input stream buffer
     stream.notWritable=Connection [{0}], Stream [{1}], This stream is not writable
     stream.outputBuffer.flush.debug=Connection [{0}], Stream [{1}], flushing output with buffer at position [{2}], writeInProgress [{3}] and closed [{4}]
    -stream.recycle=Connection [{0}], Stream [{1}] has been recycled
    +stream.recycle.duplicate=Connection [{0}], Stream [{1}] Duplicate request to recycle the associated request and response has been ignored
    +stream.recycle.first=Connection [{0}], Stream [{1}] The associated request and response have been recycled
     stream.reset.fail=Connection [{0}], Stream [{1}], Failed to reset stream
     stream.reset.receive=Connection [{0}], Stream [{1}], Reset received due to [{2}]
     stream.reset.send=Connection [{0}], Stream [{1}], Reset sent due to [{2}]
    
  • java/org/apache/coyote/http2/Stream.java+41 9 modified
    @@ -106,6 +106,9 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter {
         private volatile int urgency = Priority.DEFAULT_URGENCY;
         private volatile boolean incremental = Priority.DEFAULT_INCREMENTAL;
     
    +    private final Object recycledLock = new Object();
    +    private volatile boolean recycled = false;
    +
     
         Stream(Integer identifier, Http2UpgradeHandler handler) {
             this(identifier, handler, null);
    @@ -772,20 +775,15 @@ final void close(Http2Exception http2Exception) {
             } else {
                 handler.closeConnection(http2Exception);
             }
    -        recycle();
    +        replace();
         }
     
     
         /*
    -     * This method is called recycle for consistency with the rest of the Tomcat code base. Currently, it calls the
    -     * handler to replace this stream with an implementation that uses less memory. It does not fully recycle the Stream
    -     * ready for re-use since Stream objects are not re-used. This is useful because Stream instances are retained for a
    -     * period after the Stream closes.
    +     * This method calls the handler to replace this stream with an implementation that uses less memory. This is useful
    +     * because Stream instances are retained for a period after the Stream closes.
          */
    -    final void recycle() {
    -        if (log.isTraceEnabled()) {
    -            log.trace(sm.getString("stream.recycle", getConnectionId(), getIdAsString()));
    -        }
    +    final void replace() {
             int remaining;
             // May be null if stream was closed before any DATA frames were processed.
             ByteBuffer inputByteBuffer = getInputByteBuffer(false);
    @@ -795,6 +793,40 @@ final void recycle() {
                 remaining = inputByteBuffer.remaining();
             }
             handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state, remaining));
    +    }
    +
    +
    +    /*
    +     * This method is called recycle for consistency with the rest of the Tomcat code base. It does not recycle the
    +     * Stream since Stream objects are not re-used. It does recycle the request and response objects and ensures that
    +     * this is only done once.
    +     *
    +     * replace() should have been called before calling this method.
    +     *
    +     * It is important that this method is not called until any concurrent processing for the stream has completed. This
    +     * is currently achieved by:
    +     * - only the StreamProcessor calls this method
    +     * - the Http2UpgradeHandler does not call this method
    +     * - this method is called once the StreamProcessor considers the Stream closed
    +     *
    +     * In theory, the protection against duplicate calls is not required in this method (the code in StreamProcessor
    +     * should be sufficient) but it is implemented as precaution along with the WARN level logging.
    +     */
    +    final void recycle() {
    +        if (recycled) {
    +            log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +            return;
    +        }
    +        synchronized (recycledLock) {
    +            if (recycled) {
    +                log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +                return;
    +            }
    +            recycled = true;
    +        }
    +        if (log.isTraceEnabled()) {
    +            log.trace(sm.getString("stream.recycle.first", getConnectionId(), getIdAsString()));
    +        }
             coyoteRequest.recycle();
             coyoteResponse.recycle();
             handler.getProtocol().pushRequestAndResponse(coyoteRequest);
    
  • java/org/apache/coyote/http2/StreamProcessor.java+6 5 modified
    @@ -87,9 +87,9 @@ final void process(SocketEvent event) {
                 try {
                     /*
                      * In some scenarios, error handling may trigger multiple ERROR events for the same stream. The first
    -                 * ERROR event process will close the stream and recycle it. Once the stream has been recycled it should
    -                 * not be used for processing any further events. The check below ensures that this is the case. In
    -                 * particular, Stream.recycle() should not be called more than once per Stream.
    +                 * ERROR event processed will close the stream, replace it and recycle it. Once the stream has been
    +                 * replaced it should not be used for processing any further events. When it is known that processing is
    +                 * going to be a NO-OP, exit early.
                      */
                     if (!stream.equals(handler.getStream(stream.getIdAsInt()))) {
                         return;
    @@ -130,8 +130,8 @@ final void process(SocketEvent event) {
                                 stream.close(se);
                             } else {
                                 if (!stream.isActive()) {
    -                                // stream.close() will call recycle so only need it here
    -                                stream.recycle();
    +                                // Close calls replace() so need the same call here
    +                                stream.replace();
                                 }
                             }
                         }
    @@ -146,6 +146,7 @@ final void process(SocketEvent event) {
                         state = SocketState.CLOSED;
                     } finally {
                         if (state == SocketState.CLOSED) {
    +                        stream.recycle();
                             recycle();
                         }
                     }
    
  • webapps/docs/changelog.xml+6 0 modified
    @@ -173,6 +173,12 @@
             stream nor are trailer fields for an in progress stream swallowed if the
             Connector is paused before the trailer fields are received. (markt)
           </fix>
    +      <fix>
    +        Ensure the request and response are not recycled too soon for an HTTP/2
    +        stream when a stream level error is detected during the processing of
    +        incoming HTTP/2 frames. This could lead to incorrect processing times
    +        appearing in the access log. (markt)
    +      </fix>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
47307ee27abc

Split Stream.recycle() into replace() and recycle()

https://github.com/apache/tomcatMark ThomasOct 1, 2024via ghsa
4 files changed · +55 15
  • java/org/apache/coyote/http2/LocalStrings.properties+2 1 modified
    @@ -109,7 +109,8 @@ stream.inputBuffer.signal=Data added to inBuffer when read thread is waiting. Si
     stream.inputBuffer.swallowUnread=Swallowing [{0}] bytes previously read into input stream buffer
     stream.notWritable=Connection [{0}], Stream [{1}], This stream is not writable
     stream.outputBuffer.flush.debug=Connection [{0}], Stream [{1}], flushing output with buffer at position [{2}], writeInProgress [{3}] and closed [{4}]
    -stream.recycle=Connection [{0}], Stream [{1}] has been recycled
    +stream.recycle.duplicate=Connection [{0}], Stream [{1}] Duplicate request to recycle the associated request and response has been ignored
    +stream.recycle.first=Connection [{0}], Stream [{1}] The associated request and response have been recycled
     stream.reset.fail=Connection [{0}], Stream [{1}], Failed to reset stream
     stream.reset.receive=Connection [{0}], Stream [{1}], Reset received due to [{2}]
     stream.reset.send=Connection [{0}], Stream [{1}], Reset sent due to [{2}]
    
  • java/org/apache/coyote/http2/Stream.java+41 9 modified
    @@ -110,6 +110,9 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter {
         private volatile int urgency = Priority.DEFAULT_URGENCY;
         private volatile boolean incremental = Priority.DEFAULT_INCREMENTAL;
     
    +    private final Object recycledLock = new Object();
    +    private volatile boolean recycled = false;
    +
     
         Stream(Integer identifier, Http2UpgradeHandler handler) {
             this(identifier, handler, null);
    @@ -784,20 +787,15 @@ final void close(Http2Exception http2Exception) {
             } else {
                 handler.closeConnection(http2Exception);
             }
    -        recycle();
    +        replace();
         }
     
     
         /*
    -     * This method is called recycle for consistency with the rest of the Tomcat code base. Currently, it calls the
    -     * handler to replace this stream with an implementation that uses less memory. It does not fully recycle the Stream
    -     * ready for re-use since Stream objects are not re-used. This is useful because Stream instances are retained for a
    -     * period after the Stream closes.
    +     * This method calls the handler to replace this stream with an implementation that uses less memory. This is useful
    +     * because Stream instances are retained for a period after the Stream closes.
          */
    -    final void recycle() {
    -        if (log.isTraceEnabled()) {
    -            log.trace(sm.getString("stream.recycle", getConnectionId(), getIdAsString()));
    -        }
    +    final void replace() {
             int remaining;
             // May be null if stream was closed before any DATA frames were processed.
             ByteBuffer inputByteBuffer = getInputByteBuffer(false);
    @@ -807,6 +805,40 @@ final void recycle() {
                 remaining = inputByteBuffer.remaining();
             }
             handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state, remaining));
    +    }
    +
    +
    +    /*
    +     * This method is called recycle for consistency with the rest of the Tomcat code base. It does not recycle the
    +     * Stream since Stream objects are not re-used. It does recycle the request and response objects and ensures that
    +     * this is only done once.
    +     *
    +     * replace() should have been called before calling this method.
    +     *
    +     * It is important that this method is not called until any concurrent processing for the stream has completed. This
    +     * is currently achieved by:
    +     * - only the StreamProcessor calls this method
    +     * - the Http2UpgradeHandler does not call this method
    +     * - this method is called once the StreamProcessor considers the Stream closed
    +     *
    +     * In theory, the protection against duplicate calls is not required in this method (the code in StreamProcessor
    +     * should be sufficient) but it is implemented as precaution along with the WARN level logging.
    +     */
    +    final void recycle() {
    +        if (recycled) {
    +            log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +            return;
    +        }
    +        synchronized (recycledLock) {
    +            if (recycled) {
    +                log.warn(sm.getString("stream.recycle.duplicate", getConnectionId(), getIdAsString()));
    +                return;
    +            }
    +            recycled = true;
    +        }
    +        if (log.isTraceEnabled()) {
    +            log.trace(sm.getString("stream.recycle.first", getConnectionId(), getIdAsString()));
    +        }
             coyoteRequest.recycle();
             coyoteResponse.recycle();
             handler.getProtocol().pushRequestAndResponse(coyoteRequest);
    
  • java/org/apache/coyote/http2/StreamProcessor.java+6 5 modified
    @@ -87,9 +87,9 @@ final void process(SocketEvent event) {
                 try {
                     /*
                      * In some scenarios, error handling may trigger multiple ERROR events for the same stream. The first
    -                 * ERROR event process will close the stream and recycle it. Once the stream has been recycled it should
    -                 * not be used for processing any further events. The check below ensures that this is the case. In
    -                 * particular, Stream.recycle() should not be called more than once per Stream.
    +                 * ERROR event processed will close the stream, replace it and recycle it. Once the stream has been
    +                 * replaced it should not be used for processing any further events. When it is known that processing is
    +                 * going to be a NO-OP, exit early.
                      */
                     if (!stream.equals(handler.getStream(stream.getIdAsInt()))) {
                         return;
    @@ -130,8 +130,8 @@ final void process(SocketEvent event) {
                                 stream.close(se);
                             } else {
                                 if (!stream.isActive()) {
    -                                // stream.close() will call recycle so only need it here
    -                                stream.recycle();
    +                                // Close calls replace() so need the same call here
    +                                stream.replace();
                                 }
                             }
                         }
    @@ -146,6 +146,7 @@ final void process(SocketEvent event) {
                         state = SocketState.CLOSED;
                     } finally {
                         if (state == SocketState.CLOSED) {
    +                        stream.recycle();
                             recycle();
                         }
                     }
    
  • webapps/docs/changelog.xml+6 0 modified
    @@ -164,6 +164,12 @@
             stream nor are trailer fields for an in progress stream swallowed if the
             Connector is paused before the trailer fields are received. (markt)
           </fix>
    +      <fix>
    +        Ensure the request and response are not recycled too soon for an HTTP/2
    +        stream when a stream level error is detected during the processing of
    +        incoming HTTP/2 frames. This could lead to incorrect processing times
    +        appearing in the access log. (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

8

News mentions

0

No linked articles in our index yet.