File CVE-2025-13836-http-resp-cont-len.patch of Package python3.42038
From b3a7998115e195c40e00cfa662bcaa899d937c05 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Mon, 1 Dec 2025 17:26:07 +0200
Subject: [PATCH] gh-119451: Fix a potential denial of service in http.client
(GH-119454)
Reading the whole body of the HTTP response could cause OOM if
the Content-Length value is too large even if the server does not send
a large amount of data. Now the HTTP client reads large data by chunks,
therefore the amount of consumed memory is proportional to the amount
of sent data.
(cherry picked from commit 5a4c4a033a4a54481be6870aa1896fad732555b5)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Lib/http/client.py | 32 ++
Lib/test/test_httplib.py | 116 ++++++++++
Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5
Lib/http/client.py | 32 +++-
Lib/socket.py | 56 ++++++++
Lib/test/test_httplib.py | 67 ++++++++++
Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5
Lib/http/client.py | 32 +++-
Lib/socket.py | 55 ++++++++
Lib/test/test_httplib.py | 67 ++++++++++
Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5
Lib/http/client.py | 32 +++-
Lib/socket.py | 55 ++++++++
Lib/test/test_httplib.py | 67 ++++++++++
Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5
Lib/http/client.py | 32 +++-
Lib/socket.py | 62 +++++++++
Lib/test/test_httplib.py | 68 ++++++++++
Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5
4 files changed, 159 insertions(+), 8 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
Index: Python-3.6.15/Lib/http/client.py
===================================================================
--- Python-3.6.15.orig/Lib/http/client.py 2025-12-20 21:50:29.594974409 +0100
+++ Python-3.6.15/Lib/http/client.py 2025-12-20 21:51:56.521473590 +0100
@@ -113,6 +113,11 @@
_MAXLINE = 65536
_MAXHEADERS = 100
+# Data larger than this will be read in chunks, to prevent extreme
+# overallocation.
+_MIN_READ_BUF_SIZE = 1 << 20
+
+
# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
#
# VCHAR = %x21-7E
@@ -625,14 +630,25 @@
reading. If the bytes are truly not available (due to EOF), then the
IncompleteRead exception can be used to detect the problem.
"""
- s = []
- while amt > 0:
- chunk = self.fp.read(min(amt, MAXAMOUNT))
- if not chunk:
- raise IncompleteRead(b''.join(s), amt)
- s.append(chunk)
- amt -= len(chunk)
- return b"".join(s)
+ cursize = min(amt, _MIN_READ_BUF_SIZE)
+ data = self.fp.read(cursize)
+ if len(data) >= amt:
+ return data
+ if len(data) < cursize:
+ raise IncompleteRead(data, amt - len(data))
+
+ data = io.BytesIO(data)
+ data.seek(0, 2)
+ while True:
+ # This is a geometric increase in read size (never more than
+ # doubling out the current length of data per loop iteration).
+ delta = min(cursize, amt - cursize)
+ data.write(self.fp.read(delta))
+ if data.tell() >= amt:
+ return data.getvalue()
+ cursize += delta
+ if data.tell() < cursize:
+ raise IncompleteRead(data.getvalue(), amt - data.tell())
def _safe_readinto(self, b):
"""Same as _safe_read, but for reading into a buffer."""
Index: Python-3.6.15/Lib/socket.py
===================================================================
--- Python-3.6.15.orig/Lib/socket.py 2025-12-20 21:50:28.817654367 +0100
+++ Python-3.6.15/Lib/socket.py 2025-12-21 22:19:32.766398604 +0100
@@ -26,6 +26,7 @@
socket.setdefaulttimeout() -- set the default timeout value
create_connection() -- connects to an address, with an optional timeout and
optional source address.
+create_server() -- create a TCP socket and bind it to a specified address.
[*] not available on all platforms!
@@ -748,3 +749,64 @@
_intenum_converter(socktype, SocketKind),
proto, canonname, sa))
return addrlist
+
+
+def create_server(address, family=None, backlog=None, reuse_port=False):
+ """Convenience function which creates a SOCK_STREAM type socket
+ bound to *address* (a 2-tuple (host, port)) and return the socket
+ object.
+
+ *family* should be either AF_INET or AF_INET6.
+ *backlog* is the queue size passed to socket.listen().
+ *reuse_port* dictates whether to use the SO_REUSEPORT socket option.
+ *dualstack_ipv6*: if true and the platform supports it, it will
+ create an AF_INET6 socket able to accept both IPv4 or IPv6
+ connections. When false it will explicitly disable this option on
+ platforms that enable it by default (e.g. Linux).
+
+ >>> with create_server(('', 8000)) as server:
+ ... while True:
+ ... conn, addr = server.accept()
+ ... # handle new connection
+ """
+ if family is None:
+ family = getattr(socket, "AF_UNSPEC", 0)
+
+ host, port = address
+ err = None
+
+ # AI_PASSIVE tells getaddrinfo we intend to bind() to this address
+ try:
+ addr_infos = getaddrinfo(
+ host, port, family, SOCK_STREAM, 0, AI_PASSIVE
+ )
+ except gaierror as e:
+ raise OSError(f"getaddrinfo failed for {address}: {e}")
+
+ for res in addr_infos:
+ af, socktype, proto, canonname, sa = res
+ sock = None
+ try:
+ sock = socket(af, socktype, proto)
+
+ # Standard server socket option
+ sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+
+ if reuse_port:
+ if hasattr(socket, "SO_REUSEPORT"):
+ sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
+
+ sock.bind(sa)
+ sock.listen(backlog if backlog is not None else SOMAXCONN)
+
+ return sock # Success!
+
+ except OSError as e:
+ err = e
+ if sock is not None:
+ sock.close()
+
+ # If we get here, no address worked
+ if err is not None:
+ raise err
+ raise OSError(f"Could not create server on {address}")
Index: Python-3.6.15/Lib/test/test_httplib.py
===================================================================
--- Python-3.6.15.orig/Lib/test/test_httplib.py 2025-12-20 21:50:31.087796745 +0100
+++ Python-3.6.15/Lib/test/test_httplib.py 2025-12-21 22:56:09.233702173 +0100
@@ -5,6 +5,7 @@
import os
import array
import socket
+import threading
import unittest
TestCase = unittest.TestCase
@@ -1167,6 +1168,72 @@
thread.join()
self.assertEqual(result, b"proxied data\n")
+ def test_large_content_length(self):
+ serv = socket.create_server((HOST, 0))
+ self.addCleanup(serv.close)
+
+ def run_server():
+ [conn, address] = serv.accept()
+ with conn:
+ while conn.recv(1024):
+ conn.sendall(
+ b"HTTP/1.1 200 Ok\r\n"
+ b"Content-Length: %d\r\n"
+ b"\r\n" % size)
+ conn.sendall(b'A' * (size//3))
+ conn.sendall(b'B' * (size - size//3))
+
+ thread = threading.Thread(target=run_server)
+ thread.start()
+ self.addCleanup(thread.join, 1.0)
+
+ conn = client.HTTPConnection(*serv.getsockname())
+ try:
+ for w in range(15, 27):
+ size = 1 << w
+ conn.request("GET", "/")
+ with conn.getresponse() as response:
+ self.assertEqual(len(response.read()), size)
+ finally:
+ conn.close()
+ thread.join(1.0)
+
+ def test_large_content_length_truncated(self):
+ serv = socket.create_server((HOST, 0))
+ self.addCleanup(serv.close)
+
+ def run_server():
+ while True:
+ [conn, address] = serv.accept()
+ with conn:
+ conn.recv(1024)
+ if not size:
+ break
+ conn.sendall(
+ b"HTTP/1.1 200 Ok\r\n"
+ b"Content-Length: %d\r\n"
+ b"\r\n"
+ b"Text" % size)
+
+ thread = threading.Thread(target=run_server)
+ thread.start()
+ self.addCleanup(thread.join, 1.0)
+
+ conn = client.HTTPConnection(*serv.getsockname())
+ try:
+ for w in range(18, 65):
+ size = 1 << w
+ conn.request("GET", "/")
+ with conn.getresponse() as response:
+ self.assertRaises(client.IncompleteRead, response.read)
+ conn.close()
+ finally:
+ conn.close()
+ size = 0
+ conn.request("GET", "/")
+ conn.close()
+ thread.join(1.0)
+
def test_putrequest_override_domain_validation(self):
"""
It should be possible to override the default validation
@@ -1991,3 +2058,4 @@
if __name__ == '__main__':
unittest.main(verbosity=2)
+
Index: Python-3.6.15/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.6.15/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst 2025-12-20 21:51:56.524131559 +0100
@@ -0,0 +1,5 @@
+Fix a potential memory denial of service in the :mod:`http.client` module.
+When connecting to a malicious server, it could cause
+an arbitrary amount of memory to be allocated.
+This could have led to symptoms including a :exc:`MemoryError`, swapping, out
+of memory (OOM) killed processes or containers, or even system crashes.