Compare commits

...

No commits in common. "c8-stream-DL1" and "c8-beta-stream-DL1" have entirely different histories.

5 changed files with 1 additions and 1857 deletions

View File

@ -1,206 +0,0 @@
From 829cfae65bde395302948c99dbf0e0e8f97a1ce0 Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
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 <jrische@redhat.com>
(cherry picked from commit 93ba7376098d9a3b6d039475e15778b0ffd024de)
---
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 a84ad4a..10a7b00 100644
--- a/kdcproxy/__init__.py
+++ b/kdcproxy/__init__.py
@@ -148,6 +148,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
@@ -173,7 +174,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
@@ -185,30 +186,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 c2b1fc0..e3fa4f7 100644
--- a/tests.py
+++ b/tests.py
@@ -20,6 +20,8 @@
# THE SOFTWARE.
import os
+import socket
+import struct
import sys
import unittest
from base64 import b64decode
@@ -123,6 +125,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.51.0

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +0,0 @@
From d66084656bd01e22a5f23119968bd939d380875c Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
Date: Mon, 18 Nov 2024 10:01:16 +0100
Subject: [PATCH] Use dedicated "kdcproxy" logger
Signed-off-by: Julien Rische <jrische@redhat.com>
(cherry picked from commit c8a69dbc0777579ba3bf3d156baed0966327ebc2)
---
kdcproxy/__init__.py | 7 +++++--
kdcproxy/config/__init__.py | 7 +++++--
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/kdcproxy/__init__.py b/kdcproxy/__init__.py
index cfec0f3..a84ad4a 100644
--- a/kdcproxy/__init__.py
+++ b/kdcproxy/__init__.py
@@ -37,6 +37,9 @@ else:
import httplib
import urlparse
+logging.basicConfig()
+logger = logging.getLogger('kdcproxy')
+
class HTTPException(Exception):
@@ -326,8 +329,8 @@ class Application:
fail_socktype = self.addr2socktypename(fail_addr)
fail_ip = fail_addr[4][0]
fail_port = fail_addr[4][1]
- logging.warning("Exchange with %s:[%s]:%d failed: %s",
- fail_socktype, fail_ip, fail_port, e)
+ logger.warning("Exchange with %s:[%s]:%d failed: %s",
+ fail_socktype, fail_ip, fail_port, e)
if reply is not None:
break
diff --git a/kdcproxy/config/__init__.py b/kdcproxy/config/__init__.py
index 5bff159..93af69a 100644
--- a/kdcproxy/config/__init__.py
+++ b/kdcproxy/config/__init__.py
@@ -32,6 +32,9 @@ except ImportError: # Python 2.x
import dns.rdatatype
import dns.resolver
+logging.basicConfig()
+logger = logging.getLogger('kdcproxy')
+
class IResolver(object):
@@ -60,14 +63,14 @@ class KDCProxyConfig(IConfig):
try:
self.__cp.read(filename)
except configparser.Error:
- logging.error("Unable to read config file: %s", filename)
+ logger.error("Unable to read config file: %s", filename)
try:
mod = self.__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
--
2.46.0

View File

@ -1,158 +0,0 @@
From c8ed63653cfdf4a55ad4cf26cb11e8938b227f13 Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
Date: Mon, 18 Nov 2024 09:38:13 +0100
Subject: [PATCH] Use exponential backoff for connection retries
Calls to socket.connect() are non-blocking, hence all subsequent calls
to socket.sendall() will fail if the target KDC service is temporarily
or indefinitely unreachable. Since the kdcproxy task uses busy-looping,
it results in the journal to be flooded with warning logs.
This commit introduces a per-socket reactivation delay which increases
exponentially as the number of reties is incremented, until timeout is
reached (i.e. 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, ...).
Signed-off-by: Julien Rische <jrische@redhat.com>
(cherry picked from commit bac3c99c1b23487e38d965a79173ce9519e19c75)
---
kdcproxy/__init__.py | 63 +++++++++++++++++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 3 deletions(-)
diff --git a/kdcproxy/__init__.py b/kdcproxy/__init__.py
index 9bc7044..cfec0f3 100644
--- a/kdcproxy/__init__.py
+++ b/kdcproxy/__init__.py
@@ -60,6 +60,13 @@ class HTTPException(Exception):
return "%d %s" % (self.code, httplib.responses[self.code])
+class SocketException(Exception):
+
+ def __init__(self, message, sock):
+ super(Exception, self).__init__(message)
+ self.sockfno = sock.fileno()
+
+
class Application:
MAX_LENGTH = 128 * 1024
SOCKTYPES = {
@@ -67,10 +74,23 @@ class Application:
"udp": socket.SOCK_DGRAM,
}
+ def addr2socktypename(self, addr):
+ ret = None
+ for name in self.SOCKTYPES:
+ if self.SOCKTYPES[name] == addr[1]:
+ ret = name
+ break
+ return ret
+
def __init__(self):
self.__resolver = MetaResolver()
def __await_reply(self, pr, rsocks, wsocks, timeout):
+ starting_time = time.time()
+ send_error = None
+ recv_error = None
+ failing_sock = None
+ reactivations = {}
extra = 0
read_buffers = {}
while (timeout + extra) > time.time():
@@ -91,6 +111,12 @@ class Application:
pass
for sock in w:
+ # Fetch reactivation tuple:
+ # 1st element: reactivation index (-1 = first activation)
+ # 2nd element: planned reactivation time (0.0 = now)
+ (rn, rt) = reactivations.get(sock, (-1, 0.0))
+ if rt > time.time():
+ continue
try:
if self.sock_type(sock) == socket.SOCK_DGRAM:
# If we proxy over UDP, remove the 4-byte length
@@ -100,8 +126,13 @@ class Application:
sock.sendall(pr.request)
extra = 10 # New connections get 10 extra seconds
except Exception as e:
- logging.warning("Conection broken while writing (%s)", e)
+ send_error = e
+ failing_sock = sock
+ reactivations[sock] = (rn + 1,
+ time.time() + 2.0**(rn + 1) / 10)
continue
+ if sock in reactivations:
+ del reactivations[sock]
rsocks.append(sock)
wsocks.remove(sock)
@@ -109,7 +140,8 @@ class Application:
try:
reply = self.__handle_recv(sock, read_buffers)
except Exception as e:
- logging.warning("Connection broken while reading (%s)", e)
+ recv_error = e
+ failing_sock = sock
if self.sock_type(sock) == socket.SOCK_STREAM:
# Remove broken TCP socket from readers
rsocks.remove(sock)
@@ -117,6 +149,21 @@ class Application:
if reply is not None:
return reply
+ if reactivations:
+ raise SocketException("Timeout while sending packets after %.2fs "
+ "and %d tries: %s" % (
+ (timeout + extra) - starting_time,
+ sum(map(lambda r: r[0],
+ reactivations.values())),
+ send_error),
+ failing_sock)
+ elif recv_error is not None:
+ raise SocketException("Timeout while receiving packets after "
+ "%.2fs: %s" % (
+ (timeout + extra) - starting_time,
+ recv_error),
+ failing_sock)
+
return None
def __handle_recv(self, sock, read_buffers):
@@ -214,6 +261,7 @@ class Application:
reply = None
wsocks = []
rsocks = []
+ sockfno2addr = {}
for server in map(urlparse.urlparse, servers):
# Enforce valid, supported URIs
scheme = server.scheme.lower().split("+", 1)
@@ -260,6 +308,7 @@ class Application:
continue
except io.BlockingIOError:
pass
+ sockfno2addr[sock.fileno()] = addr
wsocks.append(sock)
# Resend packets to UDP servers
@@ -270,7 +319,15 @@ class Application:
# Call select()
timeout = time.time() + (15 if addr is None else 2)
- reply = self.__await_reply(pr, rsocks, wsocks, timeout)
+ try:
+ reply = self.__await_reply(pr, rsocks, wsocks, timeout)
+ except SocketException as e:
+ fail_addr = sockfno2addr[e.sockfno]
+ fail_socktype = self.addr2socktypename(fail_addr)
+ fail_ip = fail_addr[4][0]
+ fail_port = fail_addr[4][1]
+ logging.warning("Exchange with %s:[%s]:%d failed: %s",
+ fail_socktype, fail_ip, fail_port, e)
if reply is not None:
break
--
2.46.0

View File

@ -14,7 +14,7 @@
Name: python-%{realname}
Version: 0.4
Release: 5%{?dist}.2
Release: 5%{?dist}
Summary: MS-KKDCP (kerberos proxy) WSGI module
License: MIT
@ -24,10 +24,6 @@ Source0: https://github.com/npmccallum/%{realname}/archive/%{realname}-%{
Patch0: Make-webtest-an-optional-dependency.patch
Patch1: Correct-addrs-sorting-to-be-by-TCP-UDP.patch
Patch2: Always-buffer-TCP-data-in-__handle_recv.patch
Patch3: Use-exponential-backoff-for-connection-retries.patch
Patch4: Use-dedicated-kdcproxy-logger.patch
Patch5: 0005-Fix-DoS-vulnerability-based-on-unbounded-TCP-bufferi.patch
Patch6: 0006-Use-DNS-discovery-for-declared-realms-only.patch
BuildArch: noarch
BuildRequires: git
@ -129,16 +125,6 @@ KDCPROXY_ASN1MOD=asn1crypto %{__python3} -m pytest
%endif
%changelog
* Wed Oct 22 2025 Julien Rische <jrische@redhat.com> - 0.4-5.2
- Use DNS discovery for declared realms only (CVE-2025-59088)
Resolves: RHEL-113657
- Fix DoS vulnerability based on unbounded TCP buffering (CVE-2025-59089)
Resolves: RHEL-113664
* Fri Nov 22 2024 Julien Rische <jrische@redhat.com> - 0.4-5.1
- Log KDC timeout only once per request
Resolves: RHEL-68634
* Fri Oct 25 2019 Robbie Harwood <rharwood@redhat.com> - 0.4-5
- Always buffer TCP data in __handle_recv()
- Resolves: #1747144