diff --git a/debian/changelog b/debian/changelog index d2fecec..caabfeb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,29 @@ +python-kdcproxy (1.0.0-1deepin2) unstable; urgency=medium + + * Fix CVE-2025-59088: Unauthenticated SSRF via Realm-Controlled DNS. + Allowing DNS discovery for any requested realm created a SSRF + vulnerability. This update: - Makes use_dns apply only to realms + declared in configuration - Adds wildcard support for realm + section names - Adds dns_realm_discovery parameter for unsafe + behavior . Upstream: + https://github.com/latchset/kdcproxy/commit/1773f28eeea72ec6efcd433d + 3b66595c44d1253f + + -- deepin-ci-robot Thu, 07 May 2026 20:53:10 +0800 + +python-kdcproxy (1.0.0-1deepin1) unstable; urgency=medium + + * Fix CVE-2025-59089: Fix DoS vulnerability based on unbounded TCP + buffering. In Application.__handle_recv(), the next part of + the TCP exchange is received and queued to the io.BytesIO + stream. This update fixes: - Interrupt receiving message after + exceeding maximum length - Only export buffer content once + receiving process has ended . Upstream: + https://github.com/latchset/kdcproxy/commit/c7675365aa20be11f0324796 + 6336c7613cac84e1 + + -- deepin-ci-robot Thu, 07 May 2026 20:52:40 +0800 + python-kdcproxy (1.0.0-1) unstable; urgency=medium * New upstream release. diff --git a/debian/patches/CVE-2025-59088.patch b/debian/patches/CVE-2025-59088.patch new file mode 100644 index 0000000..8eef095 --- /dev/null +++ b/debian/patches/CVE-2025-59088.patch @@ -0,0 +1,486 @@ +diff --git a/README b/README +index 9458ef7..bf999db 100644 +--- a/README ++++ b/README +@@ -45,25 +45,43 @@ may still need it). This permits the use of longer timeouts and prevents + possible lockouts when the KDC packets contain OTP token codes (which should + preferably be sent to only one server). + +-Automatic Configuration +------------------------ +-By default, no configuration is necessary. In this case, kdcproxy will use +-REALM DNS SRV record lookups to determine remote KDC locations. +- +-Master Configuration File ++Main Configuration File + ------------------------- +-If you wish to have more detailed configuration, the first place you can +-configure kdcproxy is the master configuration file. This file exists at the +-location specified in the environment variable KDCPROXY_CONFIG. If this +-variable is unspecified, the default locations are ++The location of kdcproxy's main configuration file is specified by the ++`KDCPROXY_CONFIG` environment variable. If not set, the default locations are + `/usr/local/etc/kdcproxy.conf` or `/etc/kdcproxy.conf`. This configuration + file takes precedence over all other configuration modules. This file is an +-ini-style configuration with a special section **[global]**. Two parameters +-are available in this section: **configs** and **use_dns**. +- +-The **use_dns** allows you to enable or disable use of DNS SRV record lookups. +- +-The **configs** parameter allows you to load other configuration modules for ++ini-style configuration with a special **[global]** section, wildcard realm ++sections, and exact realm sections. ++ ++Exact realm sections are named after the realms that kdcproxy is expected to ++receive requests for. Wildcard realm sections differ from exact realm sections ++by being prefixed by a '\*' character. Such sections will match with realms ++having either all or their final labels in common with the section. As an ++example, **[\*EXAMPLE.COM]** will match with `EXAMPLE.COM`, `SUB.EXAMPLE.COM`, ++and `SUB.SUB.EXAMPLE.COM`, but not `MYEXAMPLE.COM`. ++ ++The following parameters can be set on any of these sections, with exact realm ++parameters having higher precedence, followed by wildcard realm parameters, and ++then global parameters: ++ ++**use_dns** (boolean): Allows querying DNS SRV records (aka. DNS discovery) to ++find KDCs associated with the requested realm in case they are not explicitly ++set in the configuration (main one, or configuration module-provided). By ++default (or if explicitly enabled globally), this mechanism is **activated only ++for realms explicitly declared** in the main (an empty section named after the ++realm, or a matching wildcard realm section is enough) or module-provided ++configuration. To allow use of DNS discovery for any requested realm, see the ++**dns_realm_discovery** parameter. ++ ++**silence_port_warn** (boolean): When DNS SRV records are used to discover KDC ++addresses, kdcproxy will write a warning in the logs in case a non-standard ++port is found in the DNS response. Setting this parameter to `true` will ++silence such warnings. ++ ++The following parameters are specific to the **[global]** section: ++ ++**configs** (string): Allows you to load other configuration modules for + finding configuration in other places. The configuration modules specified in + here will have priority in the order listed. For instance, if you wished to + read configuration from MIT libkrb5, you would set the following: +@@ -71,11 +89,19 @@ read configuration from MIT libkrb5, you would set the following: + [global] + configs = mit + +-Aside from the **[global]** section, you may also specify manual configuration +-for realms. In this case, each section is the name of the realm and the +-parameters are **kerberos** or **kpasswd**. These specify the locations of the +-remote servers for krb5 AS requests and kpasswd requests, respectively. For +-example: ++**dns_realm_discovery** (boolean): When **use_dns** is not disabled globally, ++kdcproxy is allowed to query SRV records to find KDCs of the realms declared in ++its configuration only. This protects kdcproxy from attacks based on ++server-side request forgery (CVE-2025-59088). Allowing DNS discovery for ++unknown realms too is possible by also setting **dns_realm_discovery** to true, ++yet heavily discouraged: ++ ++ [global] ++ dns_realm_discovery = true ++ ++Exact realm sections have 2 specific parameters: **kerberos** and **kpasswd**. ++These specify the locations of the remote servers for Kerberos ticket requests, ++and kpasswd requests, respectively. For example: + + [EXAMPLE.COM] + kerberos = kerberos+tcp://kdc.example.com:88 +@@ -95,11 +121,10 @@ forwarding requests. The port number is optional. Possible schemes are: + MIT libkrb5 + ----------- + +-If you load the **mit** config module in the master configuration file, +-kdcproxy will also read the config using libkrb5 (usually /etc/krb5.conf). If +-this module is used, kdcproxy will respect the DNS settings from the +-**[libdefaults]** section and the realm configuration from the **[realms]** +-section. ++If you load the **mit** config module in the main configuration file, kdcproxy ++will also read the config using libkrb5 (usually /etc/krb5.conf). If this ++module is used, kdcproxy will respect the realm configuration from the ++**[realms]** section. + + For more information, see the documentation for MIT's krb5.conf. + +diff --git a/kdcproxy/config/__init__.py b/kdcproxy/config/__init__.py +index a1435b7..034fcf3 100644 +--- a/kdcproxy/config/__init__.py ++++ b/kdcproxy/config/__init__.py +@@ -20,7 +20,6 @@ + # THE SOFTWARE. + + import importlib +-import itertools + import logging + import os + +@@ -32,18 +31,35 @@ except ImportError: # Python 2.x + import dns.rdatatype + import dns.resolver + ++logging.basicConfig() ++logger = logging.getLogger('kdcproxy') ++ ++SRV_KRB = 'kerberos' ++SRV_KPWD = 'kpasswd' ++SRV_KPWD_ADM = 'kerberos-adm' ++ + + class IResolver(object): + + def lookup(self, realm, kpasswd=False): ++ # type: (str, bool) -> Iterable[str] + "Returns an iterable of remote server URIs." + raise NotImplementedError() + + + class IConfig(IResolver): + +- def use_dns(self): +- "Returns whether or not DNS should be used. Returns None if not set." ++ def realm_configured(self, realm): ++ # type: (str) -> bool ++ """Check if a realm is declared in the configuration.""" ++ raise NotImplementedError() ++ ++ def param(self, realm, param): ++ # type: (str, str) -> bool ++ """Get a configuration parameter value for a realm. ++ ++ None can be passed as realm to query global parameters only. ++ """ + raise NotImplementedError() + + +@@ -51,43 +67,142 @@ class KDCProxyConfig(IConfig): + GLOBAL = "global" + default_filenames = ["/usr/local/etc/kdcproxy.conf", "/etc/kdcproxy.conf"] + ++ GLOBAL_PARAMS = { ++ 'dns_realm_discovery': False, ++ } ++ GENERAL_PARAMS = { ++ 'use_dns': True, ++ 'silence_port_warn': False, ++ } ++ RESOLV_PARAMS = [SRV_KRB, SRV_KPWD] ++ ++ @staticmethod ++ def __get_cfg_param(cp, section, param, typ): ++ """Retrieve a typed parameter from a configuration section.""" ++ try: ++ if typ is bool: ++ return cp.getboolean(section, param) ++ elif typ is str: ++ return cp.get(section, param) ++ else: ++ raise ValueError( ++ 'Configuration parameters cannot have "%s" type' % ++ typ.__name__) ++ except configparser.Error: ++ return None ++ + def __init__(self, filenames=None): +- self.__cp = configparser.ConfigParser() ++ cp = configparser.ConfigParser() + if filenames is None: + filenames = os.environ.get("KDCPROXY_CONFIG", None) + if filenames is None: + filenames = self.default_filenames + try: +- self.__cp.read(filenames) ++ cp.read(filenames) + except configparser.Error: +- logging.error("Unable to read config file(s): %s", filenames) ++ logger.error("Unable to read config file(s): %s", filenames) + + try: +- mod = self.__cp.get(self.GLOBAL, "configs") ++ mod = cp.get(self.GLOBAL, "configs") + try: + importlib.import_module("kdcproxy.config." + mod) + except ImportError as e: +- logging.log(logging.ERROR, "Error reading config: %s" % e) ++ logger.log(logging.ERROR, "Error reading config: %s" % e) + except configparser.Error: + pass + ++ self.__config = dict() ++ ++ for section in cp.sections(): ++ self.__config.setdefault(section, {}) ++ for param in self.GENERAL_PARAMS.keys(): ++ value = self.__get_cfg_param(cp, section, param, bool) ++ if value is not None: ++ self.__config[section][param] = value ++ if section == self.GLOBAL: ++ for param in self.GLOBAL_PARAMS.keys(): ++ value = self.__get_cfg_param(cp, section, param, bool) ++ if value is not None: ++ self.__config[section][param] = value ++ elif not section.startswith('*'): ++ for service in self.RESOLV_PARAMS: ++ servers = self.__get_cfg_param(cp, section, service, str) ++ if servers: ++ self.__config[section][service] = ( ++ tuple(servers.split()) ++ ) ++ ++ def __global_forbidden(self, realm): ++ """Raise ValueError if realm name is 'global'.""" ++ if realm == self.GLOBAL: ++ raise ValueError('"%s" is not allowed as realm name' % realm) ++ + def lookup(self, realm, kpasswd=False): +- service = "kpasswd" if kpasswd else "kerberos" +- try: +- servers = self.__cp.get(realm, service) +- return map(lambda s: s.strip(), servers.strip().split(" ")) +- except configparser.Error: ++ self.__global_forbidden(realm) ++ service = SRV_KPWD if kpasswd else SRV_KRB ++ if realm in self.__config and service in self.__config[realm]: ++ return self.__config[realm][service] ++ else: + return () + +- def use_dns(self): +- try: +- return self.__cp.getboolean(self.GLOBAL, "use_dns") +- except configparser.Error: +- return None ++ def realm_configured(self, realm): ++ """Check if a realm is declared in the configuration. ++ ++ Matches exact realm sections or wildcard realm sections. ++ """ ++ self.__global_forbidden(realm) ++ ++ if realm in self.__config: ++ return True ++ ++ realm_labels = realm.split('.') ++ for i in range(len(realm_labels)): ++ rule = '*' + '.'.join(realm_labels[i:]) ++ if rule in self.__config: ++ return True ++ ++ return False ++ ++ def param(self, realm, param): ++ """Get a configuration parameter value for a realm. ++ ++ None can be passed as realm to query global parameters only. ++ Precedence: exact realm, wildcard realm, global, default. ++ """ ++ self.__global_forbidden(realm) ++ ++ if realm is not None: ++ if param in self.__config.get(realm, {}): ++ # Parameter found in realm section ++ return self.__config[realm][param] ++ ++ realm_labels = realm.split('.') ++ for i in range(len(realm_labels)): ++ rule = '*' + '.'.join(realm_labels[i:]) ++ if param in self.__config.get(rule, {}): ++ # Parameter found in realm matching rule ++ return self.__config[rule][param] ++ ++ if param in self.__config.get(self.GLOBAL, {}): ++ # Fallback to global section ++ return self.__config[self.GLOBAL][param] ++ ++ if param in self.GENERAL_PARAMS: ++ # Fallback to default value if general parameter not set ++ return self.GENERAL_PARAMS[param] ++ ++ if param in self.GLOBAL_PARAMS: ++ # Fallback to default value if global parameter not set ++ return self.GLOBAL_PARAMS[param] ++ ++ raise ValueError('Configuration parameter "%s" does not exist' % param) + + + class DNSResolver(IResolver): + ++ def __init__(self, log_warning=None): ++ self.__log_warning = log_warning ++ + def __dns(self, service, protocol, realm): + query = '_%s._%s.%s' % (service, protocol, realm) + +@@ -106,48 +221,38 @@ class DNSResolver(IResolver): + yield (host, entry.port) + + def lookup(self, realm, kpasswd=False): +- service = "kpasswd" if kpasswd else "kerberos" ++ service = SRV_KPWD if kpasswd else SRV_KRB + + for protocol in ("tcp", "udp"): +- servers = tuple(self.__dns(service, protocol, realm)) ++ sv = service ++ servers = tuple(self.__dns(sv, protocol, realm)) + if not servers and kpasswd: +- servers = self.__dns("kerberos-adm", protocol, realm) ++ sv = SRV_KPWD_ADM ++ servers = self.__dns(sv, protocol, realm) + + for host, port in servers: ++ if self.__log_warning: ++ self.__log_warning(sv, protocol, realm, kpasswd, host, ++ port) + yield "%s://%s:%d" % (service, host, port) + + + class MetaResolver(IResolver): +- SCHEMES = ("kerberos", "kerberos+tcp", "kerberos+udp", +- "kpasswd", "kpasswd+tcp", "kpasswd+udp", +- "http", "https",) + +- def __init__(self): +- self.__resolvers = [] +- for i in itertools.count(0): +- allsub = IConfig.__subclasses__() +- if not i < len(allsub): +- break ++ STANDARD_PORTS = {SRV_KRB: 88, SRV_KPWD: 464} + ++ def __init__(self): ++ self.__config = KDCProxyConfig() ++ self.__dns_resolver = DNSResolver(self.__log_warning) ++ self.__extra_configs = [] ++ for cfgcls in IConfig.__subclasses__(): ++ if cfgcls is KDCProxyConfig: ++ continue + try: +- self.__resolvers.append(allsub[i]()) ++ self.__extra_configs.append(cfgcls()) + except Exception as e: +- fmt = (allsub[i], repr(e)) +- logging.log(logging.WARNING, +- "Error instantiating %s due to %s" % fmt) +- assert self.__resolvers +- +- # See if we should use DNS +- dns = None +- for cfg in self.__resolvers: +- tmp = cfg.use_dns() +- if tmp is not None: +- dns = tmp +- break +- +- # If DNS is enabled, append the DNSResolver at the end +- if dns in (None, True): +- self.__resolvers.append(DNSResolver()) ++ logging.warning("Error instantiating %s due to %s", cfgcls, ++ repr(e)) + + def __unique(self, items): + "Removes duplicate items from an iterable while maintaining order." +@@ -158,10 +263,52 @@ class MetaResolver(IResolver): + unique.remove(item) + yield item + ++ def __silenced_port_warn(self, realm): ++ """Check if port warnings are silenced for a realm.""" ++ return self.__config.param(realm, 'silence_port_warn') ++ ++ def __log_warning(self, service, protocol, realm, kpasswd, host, port): ++ """Log a warning if a KDC uses a non-standard port.""" ++ if not self.__silenced_port_warn(realm): ++ expected_port = self.STANDARD_PORTS[SRV_KPWD if kpasswd ++ else SRV_KRB] ++ if port != expected_port: ++ logger.warning( ++ 'DNS SRV record _%s._%s.%s. points to KDC %s with ' ++ 'non-standard port %i (%i expected)', ++ service, protocol, realm, host, port, expected_port) ++ ++ def __realm_configured(self, realm): ++ """Check if realm is declared in any configuration source.""" ++ if self.__config.realm_configured(realm): ++ return True ++ for c in self.__extra_configs: ++ if c.realm_configured(realm): ++ return True ++ return False ++ ++ def __dns_discovery_allowed(self, realm): ++ """Check if DNS discovery is allowed for a realm.""" ++ return ( ++ self.__realm_configured(realm) ++ or self.__config.param(None, 'dns_realm_discovery') ++ ) and self.__config.param(realm, 'use_dns') ++ + def lookup(self, realm, kpasswd=False): +- for r in self.__resolvers: +- servers = tuple(self.__unique(r.lookup(realm, kpasswd))) ++ servers = tuple(self.__unique(self.__config.lookup(realm, kpasswd))) ++ if servers: ++ return servers ++ ++ for c in self.__extra_configs: ++ servers = tuple(self.__unique(c.lookup(realm, kpasswd))) + if servers: + return servers + ++ # The scope of realms we are allowed to use DNS discovery for depends ++ # on the configuration ++ if self.__dns_discovery_allowed(realm): ++ servers = tuple(self.__unique( ++ self.__dns_resolver.lookup(realm, kpasswd))) ++ return servers ++ + return () +diff --git a/kdcproxy/config/mit.py b/kdcproxy/config/mit.py +index 1af4167..cd80f6b 100644 +--- a/kdcproxy/config/mit.py ++++ b/kdcproxy/config/mit.py +@@ -232,19 +232,9 @@ class MITConfig(IConfig): + def __init__(self, *args, **kwargs): + self.__config = {} + with KRB5Profile() as prof: +- # Load DNS setting +- self.__config["dns"] = prof.get_bool("libdefaults", +- "dns_fallback", +- default=True) +- if "dns_lookup_kdc" in dict(prof.section("libdefaults")): +- self.__config["dns"] = prof.get_bool("libdefaults", +- "dns_lookup_kdc", +- default=True) +- + # Load all configured realms +- self.__config["realms"] = {} + for realm, values in prof.section("realms"): +- rconf = self.__config["realms"].setdefault(realm, {}) ++ rconf = self.__config.setdefault(realm, {}) + for server, hostport in values: + if server not in self.CONFIG_KEYS: + continue +@@ -261,7 +251,7 @@ class MITConfig(IConfig): + rconf.setdefault(server, []).append(parsed.geturl()) + + def lookup(self, realm, kpasswd=False): +- rconf = self.__config.get("realms", {}).get(realm, {}) ++ rconf = self.__config.get(realm, {}) + + if kpasswd: + servers = list(rconf.get('kpasswd_server', [])) +@@ -271,8 +261,13 @@ class MITConfig(IConfig): + + return tuple(servers) + +- def use_dns(self, default=True): +- return self.__config["dns"] ++ def realm_configured(self, realm): ++ """Check if a realm is declared in the MIT krb5 configuration.""" ++ return realm in self.__config ++ ++ def param(self, realm, param): ++ """Always None. MIT krb5 config only provides server addresses.""" ++ return None + + + if __name__ == "__main__": diff --git a/debian/patches/CVE-2025-59089.patch b/debian/patches/CVE-2025-59089.patch new file mode 100644 index 0000000..7b27faa --- /dev/null +++ b/debian/patches/CVE-2025-59089.patch @@ -0,0 +1,205 @@ +From c7675365aa20be11f03247966336c7613cac84e1 Mon Sep 17 00:00:00 2001 +From: Julien Rische +Date: Fri, 3 Oct 2025 17:39:36 +0200 +Subject: [PATCH] Fix DoS vulnerability based on unbounded TCP buffering + +In Application.__handle_recv(), the next part of the TCP exchange is +received and queued to the io.BytesIO stream. Then, the content of the +stream was systematically exported to a buffer. However, this buffer +is only used if the data transfer is finished, causing a waste of +processing resources if the message is received in multiple parts. + +On top of these unnecessary operations, this function does not handle +length limits properly: it accepts to receive chunks of data with both +an individual and total length larger than the maximum theoretical +length of a Kerberos message, and will continue to wait for data as long +as the input stream's length is not exactly the same as the one provided +in the header of the response (even if the stream is already longer than +the expected length). + +If the kdcproxy service is not protected against DNS discovery abuse, +the attacker could take advantage of these problems to operate a +denial-of-service attack (CVE-2025-59089). + +After this commit, kdcproxy will interrupt the receiving of a message +after it exceeds the maximum length of a Kerberos message or the length +indicated in the message header. Also it will only export the content of +the input stream to a buffer once the receiving process has ended. + +Signed-off-by: Julien Rische +--- + kdcproxy/__init__.py | 51 +++++++++++++++++++------------- + tests.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 100 insertions(+), 21 deletions(-) + +diff --git a/kdcproxy/__init__.py b/kdcproxy/__init__.py +index ce96a0c..d7fb61e 100644 +--- a/kdcproxy/__init__.py ++++ b/kdcproxy/__init__.py +@@ -149,6 +149,7 @@ class Application: + if self.sock_type(sock) == socket.SOCK_STREAM: + # Remove broken TCP socket from readers + rsocks.remove(sock) ++ read_buffers.pop(sock) + else: + if reply is not None: + return reply +@@ -174,7 +175,7 @@ class Application: + if self.sock_type(sock) == socket.SOCK_DGRAM: + # For UDP sockets, recv() returns an entire datagram + # package. KDC sends one datagram as reply. +- reply = sock.recv(1048576) ++ reply = sock.recv(self.MAX_LENGTH) + # If we proxy over UDP, we will be missing the 4-byte + # length prefix. So add it. + reply = struct.pack("!I", len(reply)) + reply +@@ -186,30 +187,38 @@ class Application: + if buf is None: + read_buffers[sock] = buf = io.BytesIO() + +- part = sock.recv(1048576) +- if not part: +- # EOF received. Return any incomplete data we have on the theory +- # that a decode error is more apparent than silent failure. The +- # client will fail faster, at least. +- read_buffers.pop(sock) +- reply = buf.getvalue() +- return reply ++ part = sock.recv(self.MAX_LENGTH) ++ if part: ++ # Data received, accumulate it in a buffer. ++ buf.write(part) + +- # Data received, accumulate it in a buffer. +- buf.write(part) ++ reply = buf.getbuffer() ++ if len(reply) < 4: ++ # We don't have the length yet. ++ return None + +- reply = buf.getvalue() +- if len(reply) < 4: +- # We don't have the length yet. +- return None ++ # Got enough data to check if we have the full package. ++ (length, ) = struct.unpack("!I", reply[0:4]) ++ length += 4 # add prefix length + +- # Got enough data to check if we have the full package. +- (length, ) = struct.unpack("!I", reply[0:4]) +- if length + 4 == len(reply): +- read_buffers.pop(sock) +- return reply ++ if length > self.MAX_LENGTH: ++ raise ValueError('Message length exceeds the maximum length ' ++ 'for a Kerberos message (%i > %i)' ++ % (length, self.MAX_LENGTH)) + +- return None ++ if len(reply) > length: ++ raise ValueError('Message length exceeds its expected length ' ++ '(%i > %i)' % (len(reply), length)) ++ ++ if len(reply) < length: ++ return None ++ ++ # Else (if part is None), EOF was received. Return any incomplete data ++ # we have on the theory that a decode error is more apparent than ++ # silent failure. The client will fail faster, at least. ++ ++ read_buffers.pop(sock) ++ return buf.getvalue() + + def __filter_addr(self, addr): + if addr[0] not in (socket.AF_INET, socket.AF_INET6): +diff --git a/tests.py b/tests.py +index cd82781..2a1ad6e 100644 +--- a/tests.py ++++ b/tests.py +@@ -20,6 +20,8 @@ + # THE SOFTWARE. + + import os ++import socket ++import struct + import unittest + from base64 import b64decode + try: +@@ -122,6 +124,74 @@ class KDCProxyWSGITests(unittest.TestCase): + kpasswd=True) + self.assertEqual(response.status_code, 503) + ++ @mock.patch("socket.getaddrinfo", return_value=addrinfo) ++ @mock.patch("socket.socket") ++ def test_tcp_message_length_exceeds_max(self, m_socket, m_getaddrinfo): ++ # Test that TCP messages with length > MAX_LENGTH raise ValueError ++ # Create a message claiming to be larger than MAX_LENGTH ++ max_len = self.app.MAX_LENGTH ++ # Length prefix claiming message is larger than allowed ++ oversized_length = max_len + 1 ++ malicious_msg = struct.pack("!I", oversized_length) ++ ++ # Mock socket to return the malicious length prefix ++ mock_sock = m_socket.return_value ++ mock_sock.recv.return_value = malicious_msg ++ mock_sock.getsockopt.return_value = socket.SOCK_STREAM ++ ++ # Manually call the receive method to test it ++ read_buffers = {} ++ with self.assertRaises(ValueError) as cm: ++ self.app._Application__handle_recv(mock_sock, read_buffers) ++ ++ self.assertIn("exceeds the maximum length", str(cm.exception)) ++ self.assertIn(str(max_len), str(cm.exception)) ++ ++ @mock.patch("socket.getaddrinfo", return_value=addrinfo) ++ @mock.patch("socket.socket") ++ def test_tcp_message_data_exceeds_expected_length( ++ self, m_socket, m_getaddrinfo ++ ): ++ # Test that receiving more data than expected raises ValueError ++ # Create a message with length = 100 but send more data ++ expected_length = 100 ++ length_prefix = struct.pack("!I", expected_length) ++ # Send more data than the length prefix indicates ++ extra_data = b"X" * (expected_length + 10) ++ malicious_msg = length_prefix + extra_data ++ ++ mock_sock = m_socket.return_value ++ mock_sock.recv.return_value = malicious_msg ++ mock_sock.getsockopt.return_value = socket.SOCK_STREAM ++ ++ read_buffers = {} ++ with self.assertRaises(ValueError) as cm: ++ self.app._Application__handle_recv(mock_sock, read_buffers) ++ ++ self.assertIn("exceeds its expected length", str(cm.exception)) ++ ++ @mock.patch("socket.getaddrinfo", return_value=addrinfo) ++ @mock.patch("socket.socket") ++ def test_tcp_eof_returns_buffered_data(self, m_socket, m_getaddrinfo): ++ # Test that EOF returns any buffered data ++ initial_data = b"\x00\x00\x00\x10" # Length = 16 ++ mock_sock = m_socket.return_value ++ mock_sock.getsockopt.return_value = socket.SOCK_STREAM ++ ++ # First recv returns some data, second returns empty (EOF) ++ mock_sock.recv.side_effect = [initial_data, b""] ++ ++ read_buffers = {} ++ # First call buffers the data ++ result = self.app._Application__handle_recv(mock_sock, read_buffers) ++ self.assertIsNone(result) # Not complete yet ++ ++ # Second call gets EOF and returns buffered data ++ result = self.app._Application__handle_recv(mock_sock, read_buffers) ++ self.assertEqual(result, initial_data) ++ # Buffer should be cleaned up ++ self.assertNotIn(mock_sock, read_buffers) ++ + + def decode(data): + data = data.replace(b'\\n', b'') +-- +2.39.5 + diff --git a/debian/patches/series b/debian/patches/series index fdffa2a..58813a6 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1 +1,2 @@ -# placeholder +CVE-2025-59089.patch +CVE-2025-59088.patch