VYPR
Low severity1.7GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

aiohttp: Payload Response Resources Are Not Closed After Mid-Body Disconnect

CVE-2026-54280

Description

When a client disconnects mid-write in aiohttp, payload resources like open files are not closed immediately, enabling temporary resource starvation.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

When a client disconnects mid-write in aiohttp, payload resources like open files are not closed immediately, enabling temporary resource starvation.

Vulnerability

In aiohttp, the Payload resource (e.g., an open file handle) is not properly closed when a client disconnects in the middle of a write operation. This affects all versions before the patch in commit a762eda524 [2]. The issue occurs in the web_response and payload modules when a Payload object is used as the response body and the client closes the connection before the payload’s write() completes [1][3].

Exploitation

An attacker only needs network access to send an HTTP request to an aiohttp server and then disconnect (close the TCP connection) before the server finishes writing the payload to the response. No authentication or special privileges are required [1]. The server’s code path that would normally close the payload resource after a successful write is bypassed, leaving the resource (e.g., a file descriptor) open until Python’s garbage collector runs [3].

Impact

On success, an attacker can cause temporary resource starvation. If the payload is backed by a limited resource such as an open file handle or a database cursor, multiple rapid disconnections can exhaust available handles, leading to denial of service until garbage collection closes the leaked resources [1][3]. The CVSS v3.1 base score is 3.1 (low), indicating a limited, temporary impact [1].

Mitigation

The vulnerability is fixed in aiohttp commit a762eda524 [2]. Users should update to a version that includes this fix. The patch modifies the write_eof method to ensure Payload.close() is called even when an error (such as a client disconnect) occurs during payload writing [2]. No workaround is documented; updating is the recommended action [1]. This CVE is not listed in the known exploited vulnerabilities (KEV) catalog at the time of publication.

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
a762eda5242f

[PR #12831/1ac92dae backport][3.14] Payload close on disconnect (#12843)

https://github.com/aio-libs/aiohttppatchback[bot]Jun 7, 2026via ghsa
3 files changed · +77 3
  • aiohttp/web_response.py+4 2 modified
    @@ -808,8 +808,10 @@ async def write_eof(self, data: bytes = b"") -> None:
             if body is None or self._must_be_empty_body:
                 await super().write_eof()
             elif isinstance(self._body, Payload):
    -            await self._body.write(self._payload_writer)
    -            await self._body.close()
    +            try:
    +                await self._body.write(self._payload_writer)
    +            finally:
    +                await self._body.close()
                 await super().write_eof()
             else:
                 await super().write_eof(cast(bytes, body))
    
  • CHANGES/12831.bugfix.rst+1 0 added
    @@ -0,0 +1 @@
    +Fixed :meth:`aiohttp.web.Response.write_eof` skipping ``Payload.close()`` when the body write was interrupted by an error or cancellation, for example when a client disconnects mid-response; the payload close hook now runs in a ``finally`` so a :class:`~aiohttp.payload.Payload` body always releases its resources -- by :user:`bdraco`.
    
  • tests/test_web_response.py+72 1 modified
    @@ -1,3 +1,4 @@
    +import asyncio
     import collections.abc
     import datetime
     import gzip
    @@ -17,7 +18,7 @@
     from aiohttp.helpers import ETag
     from aiohttp.http_writer import StreamWriter, _serialize_headers
     from aiohttp.multipart import BodyPartReader, MultipartWriter
    -from aiohttp.payload import BytesPayload, StringPayload
    +from aiohttp.payload import BytesPayload, Payload, StringPayload
     from aiohttp.test_utils import make_mocked_request
     from aiohttp.web import (
         ContentCoding,
    @@ -1434,6 +1435,76 @@ async def test_consecutive_write_eof() -> None:
         writer.write_eof.assert_called_once_with(data)
     
     
    +class _ClosingPayload(Payload):
    +    """Payload test double that records whether close() ran."""
    +
    +    def __init__(self) -> None:
    +        super().__init__(None)
    +        self.close_called = False
    +        self.started = asyncio.Event()
    +        self.release = asyncio.Event()
    +        self.fail = False
    +
    +    async def write(self, writer: AbstractStreamWriter) -> None:
    +        self.started.set()
    +        if self.fail:
    +            raise ConnectionResetError("client gone")
    +        await self.release.wait()
    +
    +    async def close(self) -> None:
    +        self.close_called = True
    +        await super().close()
    +
    +    def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
    +        assert False
    +
    +
    +async def test_write_eof_closes_payload_on_success() -> None:
    +    writer = mock.create_autospec(AbstractStreamWriter, spec_set=True, instance=True)
    +    req = make_request("GET", "/", writer=writer)
    +    payload = _ClosingPayload()
    +    payload.release.set()
    +    resp = web.Response(body=payload)
    +
    +    await resp.prepare(req)
    +    await resp.write_eof()
    +
    +    assert payload.close_called
    +    assert writer.write_eof.called
    +
    +
    +async def test_write_eof_closes_payload_on_write_error() -> None:
    +    writer = mock.create_autospec(AbstractStreamWriter, spec_set=True, instance=True)
    +    req = make_request("GET", "/", writer=writer)
    +    payload = _ClosingPayload()
    +    payload.fail = True
    +    resp = web.Response(body=payload)
    +
    +    await resp.prepare(req)
    +    with pytest.raises(ConnectionResetError):
    +        await resp.write_eof()
    +
    +    assert payload.close_called
    +    assert not writer.write_eof.called
    +
    +
    +async def test_write_eof_closes_payload_on_cancel() -> None:
    +    writer = mock.create_autospec(AbstractStreamWriter, spec_set=True, instance=True)
    +    req = make_request("GET", "/", writer=writer)
    +    payload = _ClosingPayload()
    +    resp = web.Response(body=payload)
    +
    +    await resp.prepare(req)
    +    task = asyncio.ensure_future(resp.write_eof())
    +    await payload.started.wait()
    +    task.cancel()
    +    with pytest.raises(asyncio.CancelledError):
    +        await task
    +
    +    assert payload.close_called
    +    assert not writer.write_eof.called
    +
    +
     def test_set_text_with_content_type() -> None:
         resp = Response()
         resp.content_type = "text/html"
    

Vulnerability mechanics

Root cause

"Payload.close() was not called when Payload.write() raised an exception or was cancelled, leaving limited resources unreleased."

Attack vector

An attacker opens a connection to an aiohttp server that streams a response backed by a `Payload` using a limited resource (e.g. an open file handle). The attacker then disconnects mid-write, causing a `ConnectionResetError` or `asyncio.CancelledError` inside `write_eof`. Because `Payload.close()` was not guarded by a `finally` block, the resource is never released until garbage collection runs, allowing the attacker to exhaust server resources by repeatedly opening and aborting connections [patch_id=6088945].

Affected code

The bug is in `aiohttp/web_response.py` in the `write_eof` method of `web.Response`. When the body is a `Payload` instance, `Payload.close()` was only called after a successful `Payload.write()`, so an error or cancellation during the write would skip the close entirely.

What the fix does

The patch wraps `await self._body.write(...)` in a `try` block and moves `await self._body.close()` into the `finally` clause [patch_id=6088945]. This guarantees that `Payload.close()` is always called, even when the write raises a `ConnectionResetError` (client disconnect) or an `asyncio.CancelledError` (task cancellation). The changelog entry confirms the fix ensures a `Payload` body always releases its resources [ref_id=1].

Preconditions

  • configThe server must use a Payload subclass that holds a limited resource (e.g. an open file descriptor).
  • networkThe attacker must be able to open a TCP connection to the server and initiate a response that uses such a Payload.
  • inputThe attacker must disconnect (or cause a cancellation) while the Payload.write() coroutine is executing.

Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.