CVE-2020-13943
Description
If an HTTP/2 client connecting to Apache Tomcat 10.0.0-M1 to 10.0.0-M7, 9.0.0.M1 to 9.0.37 or 8.5.0 to 8.5.57 exceeded the agreed maximum number of concurrent streams for a connection (in violation of the HTTP/2 protocol), it was possible that a subsequent request made on that connection could contain HTTP headers - including HTTP/2 pseudo headers - from a previous request rather than the intended headers. This could lead to users seeing responses for unexpected resources.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Tomcat HTTP/2 connections exceeding stream limits can cause header mixing, leading to users seeing responses for unintended resources.
Vulnerability
Overview
An HTTP/2 protocol violation in Apache Tomcat could allow a client that exceeds the negotiated maximum number of concurrent streams on a single connection to cause header confusion. When the stream limit was breached, the server failed to properly reset the internal header parsing state. This allowed subsequent requests on the same connection to inherit HTTP headers—including HTTP/2 pseudo headers—from an earlier request [1].
Exploitation
Details
The attack requires an HTTP/2 client that deliberately violates the SETTINGS_MAX_CONCURRENT_STREAMS parameter established during connection setup. No authentication is needed; an unauthenticated remote attacker can exploit this by opening many concurrent streams beyond the agreed limit. The flaw resides in how Tomcat's HTTP/2 implementation checked the stream count: the validation was performed in headersStart() and, if exceeded, threw a StreamException without properly recycling the header state. This left the connection in a state where the next stream's headers could be mixed with unprocessed data from the previous stream [2][3][4].
Impact
If exploited, a user making a request after the violation could receive headers that belong to a different request, including pseudo-headers such as :path or :authority. This could result in the user seeing a response intended for a different resource—potentially leading to information disclosure or inadvertent access to sensitive data. The official description notes that this could lead to users seeing responses for unexpected resources [1].
Mitigation
Apache fixed this issue in Tomcat versions 10.0.0-M8, 9.0.38, and 8.5.58. The fix moves the concurrent-stream count check out of headersStart() and into headersEnd(), ensuring that header parsing completes without interference from an already over-limit stream. Users should upgrade to these patched versions or later. No workaround is documented for unpatched releases [1][2][3][4].
AI Insight generated on May 21, 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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.tomcat:tomcat-coyoteMaven | >= 10.0.0-M1, < 10.0.0-M8 | 10.0.0-M8 |
org.apache.tomcat:tomcat-coyoteMaven | >= 9.0.0-M1, < 9.0.38 | 9.0.38 |
org.apache.tomcat:tomcat-coyoteMaven | >= 8.5.0, < 8.5.58 | 8.5.58 |
Affected products
17- osv-coords16 versionspkg:bitnami/tomcatpkg:maven/org.apache.tomcat/tomcat-coyotepkg:rpm/opensuse/tomcat10&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/tomcat&distro=openSUSE%20Leap%2015.1pkg:rpm/opensuse/tomcat&distro=openSUSE%20Leap%2015.2pkg:rpm/opensuse/tomcat&distro=openSUSE%20Tumbleweedpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-ESPOSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Web%20and%20Scripting%2015%20SP1pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Web%20and%20Scripting%2015%20SP2pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2012%20SP5pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2012%20SP5-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%2015-LTSSpkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2012%20SP5pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015pkg:rpm/suse/tomcat&distro=SUSE%20Linux%20Enterprise%20Server%20LTSS%20Extended%20Security%2012%20SP5
>= 8.5.0, < 8.5.1+ 15 more
- (no CPE)range: >= 8.5.0, < 8.5.1
- (no CPE)range: >= 10.0.0-M1, < 10.0.0-M8
- (no CPE)range: < 10.1.14-1.1
- (no CPE)range: < 9.0.36-lp151.3.33.1
- (no CPE)range: < 9.0.36-lp152.2.10.1
- (no CPE)range: < 9.0.36-8.4
- (no CPE)range: < 9.0.36-3.74.1
- (no CPE)range: < 9.0.36-3.74.1
- (no CPE)range: < 9.0.36-4.47.3
- (no CPE)range: < 9.0.36-3.12.3
- (no CPE)range: < 9.0.36-3.50.1
- (no CPE)range: < 9.0.115-3.160.1
- (no CPE)range: < 9.0.36-3.74.1
- (no CPE)range: < 9.0.36-3.50.1
- (no CPE)range: < 9.0.36-3.74.1
- (no CPE)range: < 9.0.115-3.160.1
Patches
31bbc650cbc3fMove check for current streams to end of header parsing.
3 files changed · +27 −19
java/org/apache/coyote/http2/Http2Parser.java+1 −1 modified@@ -737,7 +737,7 @@ static interface Output { HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception, IOException; void headersContinue(int payloadSize, boolean endOfHeaders); - void headersEnd(int streamId) throws ConnectionException; + void headersEnd(int streamId) throws Http2Exception; // Priority frames (also headers) void reprioritise(int streamId, int parentStreamId, boolean exclusive, int weight)
java/org/apache/coyote/http2/Http2UpgradeHandler.java+13 −11 modified@@ -1450,16 +1450,6 @@ public HeaderEmitter headersStart(int streamId, boolean headersEndStream) stream.checkState(FrameType.HEADERS); stream.receivedStartOfHeaders(headersEndStream); closeIdleStreams(streamId); - if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { - setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); - // Ignoring maxConcurrentStreams increases the overhead count - increaseOverheadCount(); - throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", - Long.toString(localSettings.getMaxConcurrentStreams())), - Http2Error.REFUSED_STREAM, streamId); - } - // Valid new stream reduces the overhead count - reduceOverheadCount(); return stream; } else { if (log.isDebugEnabled()) { @@ -1527,12 +1517,24 @@ public void headersContinue(int payloadSize, boolean endOfHeaders) { @Override - public void headersEnd(int streamId) throws ConnectionException { + public void headersEnd(int streamId) throws Http2Exception { Stream stream = getStream(streamId, connectionState.get().isNewStreamAllowed()); if (stream != null) { setMaxProcessedStream(streamId); if (stream.isActive()) { if (stream.receivedEndOfHeaders()) { + + if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); + // Ignoring maxConcurrentStreams increases the overhead count + increaseOverheadCount(); + throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", + Long.toString(localSettings.getMaxConcurrentStreams())), + Http2Error.REFUSED_STREAM, streamId); + } + // Valid new stream reduces the overhead count + reduceOverheadCount(); + processStreamOnContainerThread(stream); } }
test/org/apache/coyote/http2/TestHttp2Section_5_1.java+13 −7 modified@@ -222,11 +222,11 @@ public void testExceedMaxActiveStreams() throws Exception { // Expecting // 1 * headers // 56k-1 of body (7 * ~8k) - // 1 * error (could be in any order) - for (int i = 0; i < 8; i++) { + // 1 * error + // for a total of 9 frames (could be in any order) + for (int i = 0; i < 9; i++) { parser.readFrame(true); } - parser.readFrame(true); Assert.assertTrue(output.getTrace(), output.getTrace().contains("5-RST-[" + @@ -238,14 +238,20 @@ public void testExceedMaxActiveStreams() throws Exception { // Release the remaining body sendWindowUpdate(0, (1 << 31) - 2); - // Allow for the 8k still in the stream window + // Allow for the ~8k still in the stream window sendWindowUpdate(3, (1 << 31) - 8193); - // 192k of body (24 * 8k) - // 1 * error (could be in any order) - for (int i = 0; i < 24; i++) { + // Read until the end of stream 3 + while (!output.getTrace().contains("3-EndOfStream")) { parser.readFrame(true); } + output.clearTrace(); + + // Confirm another request can be sent once concurrency falls back below limit + sendSimpleGetRequest(7); + parser.readFrame(true); + parser.readFrame(true); + Assert.assertEquals(getSimpleResponseTrace(7), output.getTrace()); }
55911430df13Move check for current streams to end of header parsing.
3 files changed · +27 −19
java/org/apache/coyote/http2/Http2Parser.java+1 −1 modified@@ -737,7 +737,7 @@ static interface Output { HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception, IOException; void headersContinue(int payloadSize, boolean endOfHeaders); - void headersEnd(int streamId) throws ConnectionException; + void headersEnd(int streamId) throws Http2Exception; // Priority frames (also headers) void reprioritise(int streamId, int parentStreamId, boolean exclusive, int weight)
java/org/apache/coyote/http2/Http2UpgradeHandler.java+13 −11 modified@@ -1450,16 +1450,6 @@ public HeaderEmitter headersStart(int streamId, boolean headersEndStream) stream.checkState(FrameType.HEADERS); stream.receivedStartOfHeaders(headersEndStream); closeIdleStreams(streamId); - if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { - setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); - // Ignoring maxConcurrentStreams increases the overhead count - increaseOverheadCount(); - throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", - Long.toString(localSettings.getMaxConcurrentStreams())), - Http2Error.REFUSED_STREAM, streamId); - } - // Valid new stream reduces the overhead count - reduceOverheadCount(); return stream; } else { if (log.isDebugEnabled()) { @@ -1527,12 +1517,24 @@ public void headersContinue(int payloadSize, boolean endOfHeaders) { @Override - public void headersEnd(int streamId) throws ConnectionException { + public void headersEnd(int streamId) throws Http2Exception { Stream stream = getStream(streamId, connectionState.get().isNewStreamAllowed()); if (stream != null) { setMaxProcessedStream(streamId); if (stream.isActive()) { if (stream.receivedEndOfHeaders()) { + + if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); + // Ignoring maxConcurrentStreams increases the overhead count + increaseOverheadCount(); + throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", + Long.toString(localSettings.getMaxConcurrentStreams())), + Http2Error.REFUSED_STREAM, streamId); + } + // Valid new stream reduces the overhead count + reduceOverheadCount(); + processStreamOnContainerThread(stream); } }
test/org/apache/coyote/http2/TestHttp2Section_5_1.java+13 −7 modified@@ -222,11 +222,11 @@ public void testExceedMaxActiveStreams() throws Exception { // Expecting // 1 * headers // 56k-1 of body (7 * ~8k) - // 1 * error (could be in any order) - for (int i = 0; i < 8; i++) { + // 1 * error + // for a total of 9 frames (could be in any order) + for (int i = 0; i < 9; i++) { parser.readFrame(true); } - parser.readFrame(true); Assert.assertTrue(output.getTrace(), output.getTrace().contains("5-RST-[" + @@ -238,14 +238,20 @@ public void testExceedMaxActiveStreams() throws Exception { // Release the remaining body sendWindowUpdate(0, (1 << 31) - 2); - // Allow for the 8k still in the stream window + // Allow for the ~8k still in the stream window sendWindowUpdate(3, (1 << 31) - 8193); - // 192k of body (24 * 8k) - // 1 * error (could be in any order) - for (int i = 0; i < 24; i++) { + // Read until the end of stream 3 + while (!output.getTrace().contains("3-EndOfStream")) { parser.readFrame(true); } + output.clearTrace(); + + // Confirm another request can be sent once concurrency falls back below limit + sendSimpleGetRequest(7); + parser.readFrame(true); + parser.readFrame(true); + Assert.assertEquals(getSimpleResponseTrace(7), output.getTrace()); }
9d7def063b47Move check for current streams to end of header parsing.
3 files changed · +27 −19
java/org/apache/coyote/http2/Http2Parser.java+1 −1 modified@@ -647,7 +647,7 @@ static interface Output { HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception, IOException; void headersContinue(int payloadSize, boolean endOfHeaders); - void headersEnd(int streamId) throws ConnectionException; + void headersEnd(int streamId) throws Http2Exception; // Priority frames (also headers) void reprioritise(int streamId, int parentStreamId, boolean exclusive, int weight)
java/org/apache/coyote/http2/Http2UpgradeHandler.java+13 −11 modified@@ -1559,16 +1559,6 @@ public HeaderEmitter headersStart(int streamId, boolean headersEndStream) stream.checkState(FrameType.HEADERS); stream.receivedStartOfHeaders(headersEndStream); closeIdleStreams(streamId); - if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { - setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); - // Ignoring maxConcurrentStreams increases the overhead count - increaseOverheadCount(); - throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", - Long.toString(localSettings.getMaxConcurrentStreams())), - Http2Error.REFUSED_STREAM, streamId); - } - // Valid new stream reduces the overhead count - reduceOverheadCount(); return stream; } else { if (log.isDebugEnabled()) { @@ -1636,12 +1626,24 @@ public void headersContinue(int payloadSize, boolean endOfHeaders) { @Override - public void headersEnd(int streamId) throws ConnectionException { + public void headersEnd(int streamId) throws Http2Exception { Stream stream = getStream(streamId, connectionState.get().isNewStreamAllowed()); if (stream != null) { setMaxProcessedStream(streamId); if (stream.isActive()) { if (stream.receivedEndOfHeaders()) { + + if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); + // Ignoring maxConcurrentStreams increases the overhead count + increaseOverheadCount(); + throw new StreamException(sm.getString("upgradeHandler.tooManyRemoteStreams", + Long.toString(localSettings.getMaxConcurrentStreams())), + Http2Error.REFUSED_STREAM, streamId); + } + // Valid new stream reduces the overhead count + reduceOverheadCount(); + processStreamOnContainerThread(stream); } }
test/org/apache/coyote/http2/TestHttp2Section_5_1.java+13 −7 modified@@ -227,11 +227,11 @@ public void testExceedMaxActiveStreams() throws Exception { // Expecting // 1 * headers // 56k-1 of body (7 * ~8k) - // 1 * error (could be in any order) - for (int i = 0; i < 8; i++) { + // 1 * error + // for a total of 9 frames (could be in any order) + for (int i = 0; i < 9; i++) { parser.readFrame(true); } - parser.readFrame(true); Assert.assertTrue(output.getTrace(), output.getTrace().contains("5-RST-[" + @@ -243,14 +243,20 @@ public void testExceedMaxActiveStreams() throws Exception { // Release the remaining body sendWindowUpdate(0, (1 << 31) - 2); - // Allow for the 8k still in the stream window + // Allow for the ~8k still in the stream window sendWindowUpdate(3, (1 << 31) - 8193); - // 192k of body (24 * 8k) - // 1 * error (could be in any order) - for (int i = 0; i < 24; i++) { + // Read until the end of stream 3 + while (!output.getTrace().contains("3-EndOfStream")) { parser.readFrame(true); } + output.clearTrace(); + + // Confirm another request can be sent once concurrency falls back below limit + sendSimpleGetRequest(7); + parser.readFrame(true); + parser.readFrame(true); + Assert.assertEquals(getSimpleResponseTrace(7), output.getTrace()); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- lists.opensuse.org/opensuse-security-announce/2020-11/msg00002.htmlghsavendor-advisoryx_refsource_SUSEWEB
- lists.opensuse.org/opensuse-security-announce/2020-11/msg00021.htmlghsavendor-advisoryx_refsource_SUSEWEB
- github.com/advisories/GHSA-f268-65qc-98vgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-13943ghsaADVISORY
- www.debian.org/security/2021/dsa-4835ghsavendor-advisoryx_refsource_DEBIANWEB
- github.com/apache/tomcat/commit/1bbc650cbc3f08d85a1ec6d803c47ae53a84f3bbghsaWEB
- github.com/apache/tomcat/commit/55911430df13f8c9998fbdee1f9716994d2db59bghsaWEB
- github.com/apache/tomcat/commit/9d7def063b47407a09a2f9202beed99f4dcb292aghsaWEB
- lists.apache.org/thread.html/r4a390027eb27e4550142fac6c8317cc684b157ae314d31514747f307%40%3Cannounce.tomcat.apache.org%3Eghsax_refsource_MISCWEB
- lists.debian.org/debian-lts-announce/2020/10/msg00019.htmlghsamailing-listx_refsource_MLISTWEB
- security.netapp.com/advisory/ntap-20201016-0007ghsaWEB
- security.netapp.com/advisory/ntap-20201016-0007/mitrex_refsource_CONFIRM
- www.oracle.com/security-alerts/cpuApr2021.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.