Oct 9, 2026·6 min read·5 visits
A state leak in Strawberry GraphQL's legacy WebSocket handler causes naturally completed subscriptions to permanently consume connection slots under max_subscriptions_per_connection, enabling remote attackers to deny future subscription requests on persistent sessions.
An uncontrolled resource consumption vulnerability in Strawberry GraphQL allows unauthenticated remote attackers to trigger a Denial of Service on persistent WebSocket connections using the legacy graphql-ws protocol. When the server enforces max_subscriptions_per_connection, naturally terminating subscriptions are not cleared from memory registries, leading to exhaustion of connection slots.
Strawberry GraphQL is a Python library designed for building GraphQL APIs using type hints. Among its features, Strawberry supports real-time subscriptions over WebSockets. In asynchronous network environments, subscription endpoints often expose a substantial attack surface, as persistent connections consume system resources like memory sockets and task descriptors over extended periods. To protect these resources, developers can enforce limits such as max_subscriptions_per_connection to control the allocation of active streams per connection.
The vulnerability resides within Strawberry's implementation of the legacy graphql-ws WebSocket subscription protocol handler, specifically in the file strawberry/subscriptions/protocols/graphql_ws/handlers.py. This component manages the lifecycle of subscription generators and their corresponding execution tasks. However, under configurations where a connection-level subscription limit is enforced, the handler does not maintain clean lifecycle tracking.
Classified under CWE-400 (Uncontrolled Resource Consumption), the defect allows an attacker to exhaust the configured subscription capacity of a single persistent WebSocket connection. Because the internal registry fails to release reference mappings when subscriptions naturally terminate or complete, subsequent query operations on that connection are blocked. This creates a localized, application-level Denial of Service (DoS) condition on the affected session.
To execute subscriptions, Strawberry spawns asynchronous generators wrapped in asyncio.Task objects. The legacy graphql-ws handler tracks these operations using two distinct internal dictionary attributes: self.subscriptions, which maps an operation identifier to the active execution generator, and self.tasks, which maps the operation identifier to the driving asyncio.Task loop.
When a client issues a stop command, the handler invokes cleanup_operation(operation_id). This function closes the underlying generator, cancels the associated task, and deletes both dictionary entries. However, the handler handles naturally terminating subscriptions (such as single-yield subscriptions or finished query streams) through a different execution path inside the handle_async_results coroutine.
When a subscription completes naturally, the async for loop in handle_async_results terminates, and the handler transmits a CompleteMessage to the client. In the vulnerable versions, the function exits without performing any dictionary cleanup. Although the execution task has completed and exited the event loop, its reference persists within self.tasks and self.subscriptions. If the client submits multiple consecutive completed operations using unique IDs, the self.tasks mapping grows monotonically, eventually exhausting the limit specified by max_subscriptions_per_connection and blocking all subsequent subscription requests on that connection.
The state leak within the execution pathway of the legacy handler is illustrated by the following diagram:
The vulnerability is located within the handle_async_results coroutine before the implementation of the patch. In the vulnerable version, the final execution path lacked any mechanism to clean up the bookkeeping entries when the loop finished running:
# Vulnerable code in strawberry/subscriptions/protocols/graphql_ws/handlers.py
async def handle_async_results(
self,
operation_id: str,
result_source: AsyncIterator[ExecutionResult]
) -> None:
try:
async for result in result_source:
await self.send_message(DataMessage(type="data", id=operation_id, ...))
# Natural termination point of the generator loop
await self.send_message(CompleteMessage(type="complete", id=operation_id))
# [CRITICAL FLAW] Missing dictionary cleanup for self.subscriptions and self.tasks
except asyncio.CancelledError:
await self.send_message(CompleteMessage(type="complete", id=operation_id))The official fix resolved this omission by introducing a finally block to ensure dictionary cleanup under all termination scenarios, and by refactoring the cleanup_operation function to utilize safe, non-raising .pop() methods:
# Patched code in strawberry/subscriptions/protocols/graphql_ws/handlers.py
async def handle_async_results(
self,
operation_id: str,
result_source: AsyncIterator[ExecutionResult]
) -> None:
try:
async for result in result_source:
await self.send_message(DataMessage(type="data", id=operation_id, ...))
await self.send_message(CompleteMessage(type="complete", id=operation_id))
except asyncio.CancelledError:
await self.send_message(CompleteMessage(type="complete", id=operation_id))
finally:
# Ensure the bookkeeping is released upon any exit path
self.subscriptions.pop(operation_id, None)
self.tasks.pop(operation_id, None)This remediation provides complete coverage because python guarantees the execution of the finally block, whether the coroutine completes normally, receives a cancellation signal, or raises an unhandled exception. This design ensures that task counts cannot be artificially inflated.
Exploitation of this vulnerability requires that the target server utilizes the legacy graphql-ws sub-protocol and has max_subscriptions_per_connection configured to a non-zero integer. No authentication or special privileges are required to initiate the attack sequence.
An attacker initiates a standard WebSocket handshake targeting the /graphql route, requesting the graphql-ws sub-protocol during negotiation. Once connection acknowledgment is received, the attacker begins issuing sequential subscription initiation payloads using distinct id values. Each payload requests a query or subscription that immediately finishes, such as a query yielding a single static value.
After each sequence, the server returns the requested payload followed by a complete message. Because of the state leak, the server retains the mapping for each unique operation ID. After sending a number of operations equal to the server's configured limit, any subsequent start commands issued on that WebSocket connection are met with a Subscription limit reached error message. This effectively denies the user session the ability to perform legitimate operations until the connection is terminated and re-established.
The overall security impact of CVE-2026-107727 is classified as low, as reflected by its CVSS base score of 3.7. The vulnerability represents an availability-only risk (A:L) and does not threaten data confidentiality or integrity. Furthermore, exploitation is constrained to the attacker's own persistent connection session.
In standard deployment configurations, an attacker cannot target other users' active WebSockets. However, in deployments that utilize shared connection models, multiplexing reverse-proxies, or persistent connection pooling where multiple client logical sessions share a physical socket connection, the risk of cross-user impact increases. In such scenarios, an attacker can intentionally exhaust the pool, causing denial of service to other users sharing the gateway.
Because of the low impact and the high prerequisite of requiring legacy subscription configurations, the vulnerability has an extremely low EPSS score of 0.35%. It is not known to be exploited in the wild, nor is it associated with any active ransomware campaigns. However, it represents an architectural flaw that can cause operational instability under benign client usage patterns if client applications perform frequent subscription teardowns and initializations over persistent connections.
The primary resolution for CVE-2026-107727 is upgrading the Strawberry GraphQL library to version 0.327.2 or later, which incorporates the proper task release logic inside the execution handler.
If upgrading is not immediately possible, three primary mitigations can be implemented:
Disable the connection cap setting by removing the configuration value for max_subscriptions_per_connection on the GraphQL server endpoint. While the dictionary memory leak will still occur, the server will no longer block subsequent incoming subscriptions.
Migrate to the modern, specification-compliant graphql-transport-ws protocol, which is unaffected by this state-tracking bug and is natively supported by modern GraphQL clients.
Implement a client-side workaround that explicitly transmits a legacy stop frame whenever a subscription completes or yields its final payload, forcing the server to trigger its cleanup_operation subroutine.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
Strawberry GraphQL strawberry-graphql | >= 0.312.3, < 0.327.2 | 0.327.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 3.7 (Low) |
| EPSS Score | 0.0035 (Percentile: 26.60%) |
| Exploit Status | PoC available |
| CISA KEV Status | Not listed |
The product does not properly control the allocation and maintenance of a limited resource, enabling an actor to influence the amount of resources consumed.
An authorization bypass vulnerability in Strawberry GraphQL between versions 0.217.0 and 0.326.1 occurs when a synchronous permission handler returns an awaitable object (such as an unawaited coroutine). Due to Python's truthiness rules, the unawaited coroutine is evaluated as True, leading to an immediate bypass of security policies.
A high-severity type-confusion vulnerability exists in NearForm fast-jwt prior to version 6.3.0. The vulnerability allows attackers to bypass crucial claim validation steps (such as expiration, issuer, audience, and subject validations) by presenting a validly signed JSON Web Token structured as a JSON array instead of a JSON object. This occurs because the library's decoder fails to reject JSON arrays during type evaluation.
NearForm fast-jwt prior to version 6.3.0 is vulnerable to an input validation flaw where configuring verifier properties (such as clockTolerance, clockTimestamp, and cacheTTL) with non-finite values like Infinity or NaN allows attackers to bypass temporal claim validations, including expiration (exp) and activation (nbf) boundaries. This validation bypass can result in unauthorized session persistence and cache poisoning.
A critical cryptographic vulnerability in fast-jwt versions 6.2.x prior to 6.3.0 allows unauthenticated remote attackers to execute an asymmetric-to-symmetric algorithm confusion attack due to incomplete validation of leading non-whitespace prefixes.
CVE-2026-107720 is a critical signature verification bypass vulnerability in NearForm's fast-jwt Node.js library. Under specific configurations where the token verifier is initialized with a falsy cryptographic key (such as an empty string or null) and a non-empty algorithms allowlist, the library erroneously skips signature validation. This allows unauthenticated remote attackers to submit fabricated, unsigned JSON Web Tokens and bypass the authorization boundary of the application entirely.
Ruby Mechanize prior to version 2.14.1 contains an information disclosure vulnerability. When executing cross-origin HTTP redirects, global headers configured on the Mechanize agent (such as Authorization or Session Cookies) are dynamically re-applied to the subsequent request, bypassing the internal header-stripping logic. An attacker who controls a redirection endpoint can capture sensitive bearer tokens or cookies.