VYPR
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.

PackageAffected versionsPatched versions
aiohttpPyPI
< 3.13.43.13.4

Affected products

1
  • cpe:2.3:a:aiohttp:aiohttp:*:*:*:*:*:*:*:*
    Range: <3.13.4

Patches

1
c4d77c353312

Bound DNS cache (#12106) (#12117)

https://github.com/aio-libs/aiohttpSam BullFeb 22, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.