High severity7.5NVD Advisory· Published Apr 1, 2026· Updated Apr 15, 2026
CVE-2026-34513
CVE-2026-34513
Description
AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, an unbounded DNS cache could result in excessive memory usage possibly resulting in a DoS situation. This issue has been patched in version 3.13.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
aiohttpPyPI | < 3.13.4 | 3.13.4 |
Affected products
1Patches
1c4d77c353312Bound DNS cache (#12106) (#12117)
3 files changed · +95 −6
aiohttp/connector.py+18 −6 modified@@ -856,25 +856,33 @@ async def _create_connection( class _DNSCacheTable: - def __init__(self, ttl: Optional[float] = None) -> None: - self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[ResolveResult], int]] = {} + def __init__(self, ttl: Optional[float] = None, max_size: int = 1000) -> None: + self._addrs_rr: OrderedDict[ + Tuple[str, int], Tuple[Iterator[ResolveResult], int] + ] = OrderedDict() self._timestamps: Dict[Tuple[str, int], float] = {} self._ttl = ttl + self._max_size = max_size def __contains__(self, host: object) -> bool: return host in self._addrs_rr def add(self, key: Tuple[str, int], addrs: List[ResolveResult]) -> None: + if key in self._addrs_rr: + self._addrs_rr.move_to_end(key) + self._addrs_rr[key] = (cycle(addrs), len(addrs)) if self._ttl is not None: self._timestamps[key] = monotonic() + if len(self._addrs_rr) > self._max_size: + oldest_key, _ = self._addrs_rr.popitem(last=False) + self._timestamps.pop(oldest_key, None) + def remove(self, key: Tuple[str, int]) -> None: self._addrs_rr.pop(key, None) - - if self._ttl is not None: - self._timestamps.pop(key, None) + self._timestamps.pop(key, None) def clear(self) -> None: self._addrs_rr.clear() @@ -885,6 +893,7 @@ def next_addrs(self, key: Tuple[str, int]) -> List[ResolveResult]: addrs = list(islice(loop, length)) # Consume one more element to shift internal state of `cycle` next(loop) + self._addrs_rr.move_to_end(key) return addrs def expired(self, key: Tuple[str, int]) -> bool: @@ -973,6 +982,7 @@ def __init__( fingerprint: Optional[bytes] = None, use_dns_cache: bool = True, ttl_dns_cache: Optional[int] = 10, + dns_cache_max_size: int = 1000, family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC, ssl_context: Optional[SSLContext] = None, ssl: Union[bool, Fingerprint, SSLContext] = True, @@ -1011,7 +1021,9 @@ def __init__( self._resolver_owner = False self._use_dns_cache = use_dns_cache - self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache) + self._cached_hosts = _DNSCacheTable( + ttl=ttl_dns_cache, max_size=dns_cache_max_size + ) self._throttle_dns_futures: Dict[ Tuple[str, int], Set["asyncio.Future[None]"] ] = {}
CHANGES/12106.feature.rst+1 −0 added@@ -0,0 +1 @@ +Added a ``dns_cache_max_size`` parameter to ``TCPConnector`` to limit the size of the cache -- by :user:`Dreamsorcerer`.
tests/test_connector.py+76 −0 modified@@ -4036,6 +4036,25 @@ async def handler(request): class TestDNSCacheTable: + host1 = ("localhost", 80) + host2 = ("foo", 80) + result1: ResolveResult = { + "hostname": "localhost", + "host": "127.0.0.1", + "port": 80, + "family": socket.AF_INET, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + result2: ResolveResult = { + "hostname": "foo", + "host": "127.0.0.2", + "port": 80, + "family": socket.AF_INET, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + @pytest.fixture def dns_cache_table(self): return _DNSCacheTable() @@ -4121,6 +4140,63 @@ def test_next_addrs_single(self, dns_cache_table) -> None: addrs = dns_cache_table.next_addrs("foo") assert addrs == ["127.0.0.1"] + def test_max_size_eviction(self) -> None: + table = _DNSCacheTable(max_size=2) + + table.add(self.host1, [self.result1]) + table.add(self.host2, [self.result2]) + + host3 = ("example.com", 80) + result3: ResolveResult = { + **self.result1, + "hostname": "example.com", + "host": "1.2.3.4", + } + table.add(host3, [result3]) + + assert len(table._addrs_rr) == 2 + assert self.host1 not in table._addrs_rr + assert host3 in table._addrs_rr + + def test_lru_eviction(self) -> None: + table = _DNSCacheTable(max_size=2) + + table.add(self.host1, [self.result1]) + table.add(self.host2, [self.result2]) + + table.next_addrs(self.host1) + + host3 = ("example.com", 80) + result3: ResolveResult = { + **self.result1, + "hostname": "example.com", + "host": "1.2.3.4", + } + table.add(host3, [result3]) + + assert self.host1 in table._addrs_rr + assert self.host2 not in table._addrs_rr + + def test_lru_eviction_add(self) -> None: + table = _DNSCacheTable(max_size=2) + + table.add(self.host1, [self.result1]) + table.add(self.host2, [self.result2]) + + # Re-add, thus making host1 the most recently used. + table.add(self.host1, [self.result1]) + + host3 = ("example.com", 80) + result3: ResolveResult = { + **self.result1, + "hostname": "example.com", + "host": "1.2.3.4", + } + table.add(host3, [result3]) + + assert self.host1 in table._addrs_rr + assert self.host2 not in table._addrs_rr + async def test_connector_cache_trace_race(): class DummyTracer:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/aio-libs/aiohttp/commit/c4d77c3533122be353b8afca8e8675e3b4cbda98nvdPatchWEB
- github.com/aio-libs/aiohttp/security/advisories/GHSA-hcc4-c3v8-rx92nvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-hcc4-c3v8-rx92ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34513ghsaADVISORY
- github.com/aio-libs/aiohttp/releases/tag/v3.13.4nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.