GHSA-vpp8-g58w-m8mp: HTTP/2 Rapid Reset Denial of Service in Twisted Web

Vulnerability ID: GHSA-VPP8-G58W-M8MP
CVSS Score: 7.5
Published: 2026-10-29

Twisted versions up to and including 26.4.0 are vulnerable to an unauthenticated remote denial-of-service attack via the HTTP/2 Rapid Reset technique (a variant of CVE-2023-44487). The HTTP/2 connection handler in twisted.web processes pipelined HEADERS and RST_STREAM frames sequentially within the same TCP segment. Because the request dispatch and synchronous rendering occur upon receiving the StreamEnded event before the subsequent StreamReset event is evaluated, an attacker can force the server to expend CPU cycles rendering responses while immediately evicting stream state to bypass concurrent stream limits.

TL;DR

An unauthenticated remote attacker can saturate host CPU cores in Twisted HTTP/2 servers (<= 26.4.0) by sending a continuous stream of HTTP/2 HEADERS frames paired with RST_STREAM frames over a single TCP connection, bypassing MAX_CONCURRENT_STREAMS limits.

⚠️ Exploit Status: POC

Technical Details

  • Advisory ID: GHSA-vpp8-g58w-m8mp
  • Vulnerability Class: CWE-400: Uncontrolled Resource Consumption (HTTP/2 Rapid Reset)
  • CVSS v3.1 Score: 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
  • Attack Vector: Network (Unauthenticated HTTP/2 Connection)
  • Affected Component: twisted.web._http2.H2Connection (<= 26.4.0)
  • Patched Version: Twisted >= 26.10.0rc1
  • Exploit Status: Proof of Concept Available
  • Related CVE: CVE-2023-44487

Affected Systems

  • Twisted (twisted.web) <= 26.4.0
  • Twisted: <= 26.4.0 (Fixed in: 26.10.0rc1)

Code Analysis

Commit: 4989394

Implement token bucket rate limiting for HTTP/2 peer stream resets (RST_STREAM) to mitigate Rapid Reset DoS

diff --git a/src/twisted/web/_http2.py b/src/twisted/web/_http2.py index 954cbd2255b..103e681cbef 100644 --- a/src/twisted/web/_http2.py +++ b/src/twisted/web/_http2.py @@ -111,6 +111,11 @@ class H2Connection(Protocol, TimeoutMixin): _log = Logger() _abortingCall = None  + # Token bucket that rate-limits peer stream resets (RST_STREAM) to defend + # against HTTP/2 Rapid Reset (CVE-2023-44487), the same shape nghttp2 uses. + _resetTokenBurst = 1000 + _resetTokenRate = 33 +  def __init__(self, reactor=None): config = h2.config.H2Configuration(client_side=False, header_encoding=None) self.conn = h2.connection.H2Connection(config=config) @@ -133,6 +138,10 @@ def __init__(self, reactor=None): from twisted.internet import reactor self._reactor = reactor  + # Reset-rate token bucket, starts full. + self._resetTokens = float(self._resetTokenBurst) + self._resetTokenTimestamp = self._reactor.seconds() +  # Start the data sending function. self._reactor.callLater(0, self._sendPrioritisedData)  @@ -159,10 +168,7 @@ def dataReceived(self, data): try: events = self.conn.receive_data(data) except h2.exceptions.ProtocolError: - stillActive = self._tryToWriteControlData() - if stillActive: - self.transport.loseConnection() - self.connectionLost(Failure(), _cancelTimeouts=False) + self._terminate(Failure())  return   # Only reset the timeout if we've received an actual H2 @@ -499,6 +505,25 @@ def _requestAborted(self, event): ) self._requestDone(event.stream_id)  + # Refill for elapsed time, then spend one token. An empty bucket means a + # Rapid Reset flood (CVE-2023-44487), so drop the connection with GOAWAY. + now = self._reactor.seconds() + # reactor.seconds() is a wall clock, so clamp elapsed to stop a backward + # NTP step from draining the bucket and disconnecting a well-behaved peer. + elapsed = max(0.0, now - self._resetTokenTimestamp) + self._resetTokens = min( + self._resetTokenBurst, + self._resetTokens + elapsed * self._resetTokenRate, + ) + self._resetTokenTimestamp = now + if self._resetTokens < 1: + self.conn.close_connection( + error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM + ) + self._terminate(Failure(ConnectionLost("Too many stream resets"))) + else: + self._resetTokens -= 1 +  def _handlePriorityUpdate(self, event): """ Internal handler for when a stream priority is updated. @@ -780,6 +805,18 @@ def _streamIsActive(self, streamID): """ return streamID in self.streams  + def _terminate(self, reason): + """ + Flush any pending control data and tear the connection down, leaving + timers armed so that a peer who never reads our data is timed out. + + @param reason: The failure to report to the connection's streams. + @type reason: L{Failure} + """ + if self._tryToWriteControlData(): + self.transport.loseConnection() + self.connectionLost(reason, _cancelTimeouts=False) +  def _tryToWriteControlData(self): """ Checks whether the connection is blocked on flow control and, 

Mitigation Strategies

  • Upgrade Twisted to a patched release that enforces token-bucket rate limiting on incoming HTTP/2 RST_STREAM frames.
  • Terminate public-facing HTTP/2 connections at a hardened reverse proxy (e.g., NGINX, Envoy, HAProxy) and proxy backend requests to Twisted over HTTP/1.1.
  • Disable HTTP/2 ALPN negotiation ('h2') on directly exposed Twisted TLS listeners to force HTTP/1.1 fallback.

Remediation Steps:

  1. Identify all Python environments and container images running Twisted <= 26.4.0 with the optional 'h2' dependency installed.
  2. Update dependency manifests (requirements.txt, pyproject.toml) to require 'Twisted>=26.10.0rc1'.
  3. Run 'pip install --upgrade "Twisted>=26.10.0rc1"' and restart the Twisted service processes.
  4. Verify that HTTP/2 connections sending more than 1,000 rapid resets receive a GOAWAY frame with error code ENHANCE_YOUR_CALM (0xb).

References

Read the full report for GHSA-VPP8-G58W-M8MP on our website for more details including interactive diagrams and full exploit analysis.