import UBI python-kdcproxy-1.1.0-1.el10

This commit is contained in:
AlmaLinux RelEng Bot 2026-05-19 18:54:23 -04:00
parent 828042b536
commit 238356559c
9 changed files with 28 additions and 1963 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
kdcproxy-1.0.0.tar.gz kdcproxy-1.1.0.tar.gz

View File

@ -1,206 +0,0 @@
From 697ba9115366481018dd486e2551f2d23c0addae 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 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.51.0

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
From 7b7aee01d72be5a310678cdad189cb7382f28549 Mon Sep 17 00:00:00 2001
From: Robbie Harwood <rharwood@redhat.com>
Date: Tue, 19 Jan 2021 11:41:40 -0500
Subject: [PATCH] Drop coverage from tests
To my knowledge, we've never looked at or done anything with this
output. Test coverage is a noble goal, but this project is mostly
complete, so we don't expect heavy development soon.
Requested-by: Petr Viktorin <pviktori@redhat.com>
Signed-off-by: Robbie Harwood <rharwood@redhat.com>
(cherry picked from commit 86c3da13d5d6cdb5822d194f2b820da1fd31dddb)
[rharwood@redhat.com: .gitignore]
---
.coveragerc | 23 -----------------------
MANIFEST.in | 1 -
setup.py | 2 +-
tox.ini | 12 ++----------
4 files changed, 3 insertions(+), 35 deletions(-)
delete mode 100644 .coveragerc
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 4038562..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,23 +0,0 @@
-[run]
-branch = True
-source =
- kdcproxy
- tests.py
-
-[paths]
-source =
- kdcproxy
- .tox/*/lib/python*/site-packages/kdcproxy
-
-[report]
-ignore_errors = False
-precision = 1
-exclude_lines =
- pragma: no cover
- raise AssertionError
- raise NotImplementedError
- if 0:
- if False:
- if __name__ == .__main__.:
- if PY3
- if not PY3
diff --git a/MANIFEST.in b/MANIFEST.in
index 362f840..ff6b9a7 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,4 +2,3 @@ include README COPYING
include tox.ini
include setup.cfg
include tests.py tests.krb5.conf
-include .coveragerc
diff --git a/setup.py b/setup.py
index 20b335e..4b34fcc 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@ install_requires = [
]
extras_require = {
- "tests": ["pytest", "coverage", "WebTest"],
+ "tests": ["pytest", "WebTest"],
"test_pep8": ['flake8', 'flake8-import-order', 'pep8-naming']
}
diff --git a/tox.ini b/tox.ini
index 038d996..9672cee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,21 +1,13 @@
[tox]
minversion = 2.3.1
-envlist = py36,py37,py38,py39,pep8,py3pep8,doc,coverage-report
+envlist = py36,py37,py38,py39,pep8,py3pep8,doc
skip_missing_interpreters = true
[testenv]
deps =
.[tests]
commands =
- {envpython} -m coverage run --parallel \
- -m pytest --capture=no --strict {posargs}
-
-[testenv:coverage-report]
-deps = coverage
-skip_install = true
-commands =
- {envpython} -m coverage combine
- {envpython} -m coverage report --show-missing
+ {envpython} -m pytest --capture=no --strict {posargs}
[testenv:pep8]
basepython = python3

View File

@ -1,71 +0,0 @@
From 99babf4ba3ce4d1f5bb893e7678df44d16b74d03 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 d0ca43e..ce96a0c 100644
--- a/kdcproxy/__init__.py
+++ b/kdcproxy/__init__.py
@@ -38,6 +38,9 @@ else:
import httplib
import urlparse
+logging.basicConfig()
+logger = logging.getLogger('kdcproxy')
+
class HTTPException(Exception):
@@ -327,8 +330,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 a1435b7..8e17c5b 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(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")
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 0b2efa7b2901ada01758a0525a21af5447aa647a 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 1493b30..d0ca43e 100644
--- a/kdcproxy/__init__.py
+++ b/kdcproxy/__init__.py
@@ -61,6 +61,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 = {
@@ -68,10 +75,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():
@@ -92,6 +112,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
@@ -101,8 +127,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)
@@ -110,7 +141,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)
@@ -118,6 +150,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):
@@ -215,6 +262,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)
@@ -261,6 +309,7 @@ class Application:
continue
except io.BlockingIOError:
pass
+ sockfno2addr[sock.fileno()] = addr
wsocks.append(sock)
# Resend packets to UDP servers
@@ -271,7 +320,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

@ -0,0 +1 @@
f03b9d40d71322281d0197df6fe6b5936a8d09b0fee49fc5375b61974d005cedc5645f92a223d221c05c6ffd2613a86eb7d7295c4ac27a2f2c9eaa10fa24c182 kdcproxy-1.1.0.tar.gz

View File

@ -1,33 +1,32 @@
%global realname kdcproxy %global realname kdcproxy
Name: python-%{realname} Name: python-%{realname}
Version: 1.0.0 Version: 1.1.0
Release: 19%{?dist} Release: 1%{?dist}
Summary: MS-KKDCP (kerberos proxy) WSGI module Summary: MS-KKDCP (kerberos proxy) WSGI module
License: MIT License: MIT
URL: https://github.com/latchset/%{realname} URL: https://github.com/latchset/%{realname}
Source0: https://github.com/latchset/%{realname}/archive/%{realname}-%{version}.tar.gz Source0: https://github.com/latchset/%{realname}/releases/download/v%{version}/%{realname}-%{version}.tar.gz
Source1: https://github.com/latchset/%{realname}/releases/download/v%{version}/%{realname}-%{version}.tar.gz.sha512sum.txt
Patch0001: Drop-coverage-from-tests.patch # Patches
Patch0002: Use-exponential-backoff-for-connection-retries.patch
Patch0003: Use-dedicated-kdcproxy-logger.patch
Patch0004: 0004-Fix-DoS-vulnerability-based-on-unbounded-TCP-bufferi.patch
Patch0005: 0005-Use-DNS-discovery-for-declared-realms-only.patch
BuildArch: noarch BuildArch: noarch
BuildRequires: git
BuildRequires: python3-devel BuildRequires: git-core
BuildRequires: python3-dns
BuildRequires: python3-pyasn1
BuildRequires: python3-pytest BuildRequires: python3-pytest
BuildRequires: python3-setuptools
%description %generate_buildrequires
%pyproject_buildrequires
%global _description %{expand:
This package contains a Python WSGI module for proxying KDC requests over This package contains a Python WSGI module for proxying KDC requests over
HTTP by following the MS-KKDCP protocol. It aims to be simple to deploy, with HTTP by following the MS-KKDCP protocol. It aims to be simple to deploy, with
minimal configuration. minimal configuration.
}
%description %{_description}
%package -n python3-%{realname} %package -n python3-%{realname}
Summary: MS-KKDCP (kerberos proxy) WSGI module Summary: MS-KKDCP (kerberos proxy) WSGI module
@ -36,35 +35,33 @@ Requires: python3-pyasn1
%{?python_provide:%python_provide python3-%{realname}} %{?python_provide:%python_provide python3-%{realname}}
%description -n python3-%{realname} %description -n python3-%{realname} %{_description}
This package contains a Python 3.x WSGI module for proxying KDC requests over
HTTP by following the MS-KKDCP protocol. It aims to be simple to deploy, with
minimal configuration.
%prep %prep
%autosetup -S git -n %{realname}-%{version} %autosetup -S git_am -n %{realname}-%{version}
%build %build
%py3_build %pyproject_wheel
%install %install
%py3_install %pyproject_install
%pyproject_save_files %{realname}
%check %check
%{__python3} -m pytest %pyproject_check_import
%pytest
%files -n python3-%{realname} %files -n python%{python3_pkgversion}-%{realname} -f %{pyproject_files}
%doc README %doc README
%license COPYING %license COPYING
%{python3_sitelib}/%{realname}/
%{python3_sitelib}/%{realname}-%{version}-*.egg-info
%changelog %changelog
* Mon Oct 20 2025 Julien Rische <jrische@redhat.com> - 1.0.0-19 * Wed Nov 19 2025 Julien Rische <jrische@redhat.com> - 1.1.0-1
- New upstream version (1.1.0)
- Use DNS discovery for declared realms only (CVE-2025-59088) - Use DNS discovery for declared realms only (CVE-2025-59088)
Resolves: RHEL-122777 Resolves: RHEL-113652
- Fix DoS vulnerability based on unbounded TCP buffering (CVE-2025-59089) - Fix DoS vulnerability based on unbounded TCP buffering (CVE-2025-59089)
Resolves: RHEL-122776 Resolves: RHEL-113656
* Fri Nov 22 2024 Julien Rische <jrische@redhat.com> - 1.0.0-18 * Fri Nov 22 2024 Julien Rische <jrische@redhat.com> - 1.0.0-18
- Log KDC timeout only once per request - Log KDC timeout only once per request

View File

@ -1 +1 @@
SHA512 (kdcproxy-1.0.0.tar.gz) = 617dba929d1c87c60d9a321269fd23348af11eabd8db3cea4b4750ec9514c9dce3487e658c396a5d009c9ef92326d45f5f00a2a116ab6469c62eda8270e55391 SHA512 (kdcproxy-1.1.0.tar.gz) = f03b9d40d71322281d0197df6fe6b5936a8d09b0fee49fc5375b61974d005cedc5645f92a223d221c05c6ffd2613a86eb7d7295c4ac27a2f2c9eaa10fa24c182