Compare commits
No commits in common. "c8-stream-DL1" and "c8-beta-stream-DL1" have entirely different histories.
c8-stream-
...
c8-beta-st
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user