aiohttp: Payload Response Resources Are Not Closed After Mid-Body Disconnect
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
2Patches
1a762eda5242f[PR #12831/1ac92dae backport][3.14] Payload close on disconnect (#12843)
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
3News mentions
0No linked articles in our index yet.