aiohttp 3.14.4, released on 5 October 2026, fixed eight security advisories at once. The project labeled six of them low and two medium. The CVE records that appeared on 19 October score them between 2.3 and 8.2 under CVSS 4.0, and your scanner will show whichever number its database picked up, if it shows anything yet.
This post is for teams running aiohttp servers or clients in Python. It lists the eight issues and who each one affects, explains the severity mismatch, covers the behavior changes in 3.14.4 that can surprise you on upgrade, and gives a script that checks your install and probes your own server for the idle-connection bug.
The short answer
Upgrade aiohttp to 3.14.4 or later; the latest release is 3.14.5. Versions up to 3.14.3 are affected by eight advisories published on 7 October 2026, including CVE-2026-111064, where a connection that never sent a request was never timed out, and CVE-2026-111750, request smuggling in the pure-Python parser. When you upgrade, forms with more than 1,000 fields get a 413 response by default, and idle connections are now closed after keepalive_timeout even before the first request.
The eight advisories
All eight list versions up to 3.14.3 as affected and 3.14.4 as fixed. The scores are from the CVE records, which GitHub, as the CNA, published between 16:44 and 17:06 UTC on 19 October. The labels are the project's own.
| CVE | What it is | Who is affected | Project label | CVSS 4.0 |
|---|---|---|---|---|
| CVE-2026-111064 | Server didn't apply the keepalive timeout before the first request, so an idle connection could stay open indefinitely | aiohttp servers without a proxy that limits idle connections | low | 8.2 |
| CVE-2026-111066 | File descriptor exhaustion through crafted multipart requests | Handlers that call Request.post() | medium | 8.2 |
| No CVE (GHSA-2g87-pp6c-29x5) | Several cases of excessive or unbounded memory or CPU use on crafted requests | Servers, per the advisory | medium | 8.7 in GitHub's database |
| CVE-2026-111065 | An overflow in Max-Age processing could skip closing a connection, exhausting the client's pool until garbage collection | Clients | low | 6.9 |
| CVE-2026-111750 | Request smuggling in the pure-Python HTTP parser | Servers using the Python parser: AIOHTTP_NO_EXTENSIONS set, or no standard wheel | low | 6.3 |
| CVE-2026-111748 | Absolute-form request targets could spoof Request.scheme | Apps that use Request.scheme for security decisions | low | 6.3 |
| CVE-2026-111063 | The Secure attribute wasn't enforced for the shared cookie | Clients whose cookie jar holds a shared cookie | low | 6.3 |
| CVE-2026-111749 | A subdomain could strip the Host-Only flag from a parent-domain cookie | Clients visiting untrusted subdomains | low | 2.3 |
The reporters credited across the advisories include tatulea, euriconicacio, iamibi, nemoola, denyspakizh-tob, rodrigobnogueira, alimony, EpicLiem, axel-mierczuk-1pw and afldl, with maintainer Dreamsorcerer credited on all of them.
Why the severities disagree
A project's label and a CVSS score answer different questions. The maintainers' notes suggest they weighed realistic deployments: the idle-connection advisory, for example, says the workaround is "a reverse proxy (as recommended for all production deployments)". A CVSS base score assumes the worst reachable configuration, and an aiohttp server exposed directly to the network with no proxy is exactly that.
Both views are fair. What matters is which one reaches you:
- GitHub's database reviewed three of the eight on 19 October between 17:07 and 17:08 UTC. It rates CVE-2026-111064 high (8.2) and GHSA-2g87-pp6c-29x5 high (8.7), and keeps CVE-2026-111063 at low.
- OSV had the same three when we checked at 18:58 UTC on 19 October. The other five weren't in either database yet.
- The CVE records exist for seven of the eight. The memory and CPU advisory has no CVE ID.
So on the day the CVEs appeared, a scanner reading GitHub's data or OSV would show three findings for aiohttp 3.14.3, not eight. The others will arrive as the databases catch up. One more detail: the repository advisory for CVE-2026-111066 lists the affected range as up to and including 3.14.4, while the CVE record says before 3.14.4. The CVE record matches the patched version.
The idle-connection bug, reproduced
aiohttp's keepalive_timeout closes a connection that sits idle after a request. Before 3.14.4 the timer only started after a request, so a client that connected and sent nothing was never closed. The fix starts the timer when the connection opens, and the updated docs say the same delay now bounds how long a new connection may take to deliver its first complete request.
We checked this on aiohttp 3.14.3, running a small app on localhost with keepalive_timeout=3, next to a plain asyncio server that closes idle connections after three seconds. The probe opens a connection, sends nothing, and waits. The aiohttp server was still holding the connection after 20 seconds. The control closed it at 3.0 seconds. Each idle connection holds a socket and a file descriptor, so enough of them exhaust the server.
Check your install and your server
This script reports your aiohttp version and which HTTP parser is in use, and can probe one of your own servers by opening a connection and timing how long it stays open with nothing sent. It doesn't send any request. Standard library only, apart from importing aiohttp if it's installed:
#!/usr/bin/env python3
"""Check an aiohttp install against the eight advisories fixed in 3.14.4, and
optionally probe one of YOUR servers for the idle-connection bug
(CVE-2026-111064): open a TCP connection, send nothing, and time how long the
server takes to close it.
Usage: python3 aiohttp_check.py (this environment)
python3 aiohttp_check.py --probe HOST:PORT [--max 120]
"""
import importlib.metadata as md
import os
import socket
import sys
import time
FIXED = (3, 14, 4)
def vtuple(v):
out = []
for part in v.split(".")[:3]:
digits = "".join(ch for ch in part if ch.isdigit())
out.append(int(digits or 0))
return tuple(out + [0] * (3 - len(out)))
def check_install():
try:
ver = md.version("aiohttp")
except md.PackageNotFoundError:
print("aiohttp is not installed in this environment")
return
status = "fixed" if vtuple(ver) >= FIXED else "AFFECTED, upgrade to 3.14.4 or later"
print(f"aiohttp {ver}: {status}")
try:
from aiohttp import http_parser
c_parser = http_parser.HttpRequestParser is getattr(http_parser, "HttpRequestParserC", None)
except Exception as e: # import problems shouldn't hide the version result
print(f"could not inspect the parser: {e}")
return
if os.environ.get("AIOHTTP_NO_EXTENSIONS"):
print("AIOHTTP_NO_EXTENSIONS is set: the pure-Python parser is in use (CVE-2026-111750 applies before 3.14.4)")
elif not c_parser:
print("C parser not available: the pure-Python parser is in use (CVE-2026-111750 applies before 3.14.4)")
else:
print("C HTTP parser in use")
def probe(target, max_wait):
host, _, port = target.rpartition(":")
start = time.monotonic()
with socket.create_connection((host, int(port)), timeout=10) as s:
s.settimeout(1)
while time.monotonic() - start < max_wait:
try:
if s.recv(1) == b"":
print(f"{target}: server closed the idle connection after {time.monotonic() - start:.1f}s")
return
except socket.timeout:
continue
except ConnectionResetError:
print(f"{target}: server reset the idle connection after {time.monotonic() - start:.1f}s")
return
print(f"{target}: connection still open after {max_wait}s with no request sent; "
"the server, or the proxy in front of it, doesn't time out idle connections")
def main():
args = sys.argv[1:]
if "--probe" in args:
target = args[args.index("--probe") + 1]
max_wait = float(args[args.index("--max") + 1]) if "--max" in args else 120.0
probe(target, max_wait)
else:
check_install()
if __name__ == "__main__":
main()
Our run, on the lab machine with aiohttp 3.14.3:
$ python3 aiohttp_check.py
aiohttp 3.14.3: AFFECTED, upgrade to 3.14.4 or later
C HTTP parser in use
$ AIOHTTP_NO_EXTENSIONS=1 python3 aiohttp_check.py
aiohttp 3.14.3: AFFECTED, upgrade to 3.14.4 or later
AIOHTTP_NO_EXTENSIONS is set: the pure-Python parser is in use (CVE-2026-111750 applies before 3.14.4)
$ python3 aiohttp_check.py --probe 127.0.0.1:8081 --max 20
127.0.0.1:8081: connection still open after 20.0s with no request sent; the server, or the proxy in front of it, doesn't time out idle connections
$ python3 aiohttp_check.py --probe 127.0.0.1:8082 --max 20
127.0.0.1:8082: server closed the idle connection after 3.0s
Point the probe at the address clients actually reach. If a reverse proxy sits in front of aiohttp and closes idle connections, the probe shows the proxy's timeout, which is what you want to know. If it says the connection is still open at the limit, neither layer is protecting you. Only probe servers you own or are authorized to test.
What changes when you upgrade
Most of 3.14.4 is fixes, but a few changes can show up as behavior differences:
- Forms over 1,000 fields get a 413.
Request.post()now enforces a newclient_max_fieldssetting onweb.Application, default 1,000. If you accept large forms on purpose, raise it. Setting 0 disables the cap. - Idle connections close before the first request. Clients that open a connection and wait longer than
keepalive_timeout(75 seconds by default inrun_app) before sending anything will be disconnected. That's the fix, but check long-poll or pre-connect patterns. - Multipart uploads use spooled temporary files, which reduces the file descriptors needed.
- The pure-Python parser now rejects control characters in the request target, matching the C parser.
- WebSocket and decompression limits are tighter, including bounds on frames delivered in many small reads and on payloads with many concatenated compressed members.
If you pin dependencies, the fix is one line in your lockfile. Here's Safeguard's interactive walkthrough of finding vulnerable dependencies that already have a fix available:
What to do
- Upgrade to 3.14.4 or later, and update lockfiles and container images, not just a development environment.
- Raise
client_max_fieldsbefore upgrading if any form legitimately sends more than 1,000 fields. - Make sure the C parser is in use. If
AIOHTTP_NO_EXTENSIONSis set or you're on a platform without a wheel, you were exposed to the request smuggling issue. - Put a reverse proxy with idle and header timeouts in front of aiohttp servers, as the project recommends, and probe it.
- Don't use
Request.schemealone for security decisions behind a proxy. Use the forwarded headers your proxy sets and trusts. - Don't wait for your scanner. On 19 October only three of the eight were in GitHub's database and OSV.
If you use aiohttp as a client with credentials, our post on redirect header leaks in HTTP clients covers a related class of client-side bugs.
The fair view
The maintainers did a lot right. They fixed eight issues and a long list of hardening items in one release, published advisories two days later, credited a long list of reporters, and offered workarounds where they could. Their low ratings reflect a deployment model, with a proxy in front, that they document and recommend.
The cost of that model is that the people most exposed are the ones who didn't follow the recommendation, and they're also the ones least likely to read release notes. A CVSS score of 8.2 next to a label of "low" isn't a contradiction to settle. It's two answers for two kinds of deployment. Work out which one you run.
Check yourself
Your aiohttp 3.14.3 app sits behind a reverse proxy that closes idle connections after 60 seconds. Is CVE-2026-111064 a problem?
Much less of one, because the proxy bounds idle connections before they reach aiohttp. Upgrade anyway, and run the probe against the public address to confirm the proxy's timeout is what clients see.
After upgrading, a large admin form starts returning 413. Why?
3.14.4 added client_max_fields to web.Application with a default of 1,000 form fields. Raise it for that app if large forms are expected, or set 0 to disable the cap.
Your scanner shows three aiohttp findings for 3.14.3. Are those all of them?
Not necessarily. Eight advisories were fixed in 3.14.4, and on 19 October only three had reached GitHub's database and OSV. Upgrade based on the release, not the count.