diff --git a/0003-Restrict-RPC-dispatch-to-registered-methods-only.patch b/0003-Restrict-RPC-dispatch-to-registered-methods-only.patch new file mode 100644 index 0000000..907db11 --- /dev/null +++ b/0003-Restrict-RPC-dispatch-to-registered-methods-only.patch @@ -0,0 +1,75 @@ +From 431d5a9a5f7ac4ce61210f4e21ecc64e79c8b31a Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Mon, 27 Jul 2026 18:20:44 +0200 +Subject: [PATCH] Restrict RPC dispatch to registered methods only + +get_method_implementation() used unrestricted getattr() to resolve +attacker-controlled method names on handler objects. Since server.py +binds the full connection object as the RPC handler via +connect_rpc_interface('SETroubleshootServer', self), all inherited +internal methods (acquire_write_lock, close_connection, etc.) became +callable through crafted RPC requests. + +An attacker could connect to the world-writable UNIX socket and invoke +acquire_write_lock as a signal, then send any RPC method call. The +response path calls send_data() which tries to acquire the same +non-reentrant threading.Lock, deadlocking the daemon and making it +unresponsive to all clients. + +Validate the requested method name against interface_registry before +calling getattr(). The method must exist in the registry with an +rpc_def.type matching the expected RPC type ('method' or 'signal'). +Unregistered names now fall through to the existing "not implemented" +error response instead of being dispatched. The expected_type parameter +defaults to None for backward compatibility with any callers outside +default_request_handler. + +Co-Authored-By: Claude Opus 4.6 +--- + src/setroubleshoot/rpc.py | 13 ++++++++++--- + 1 file changed, 10 insertions(+), 3 deletions(-) + +diff --git a/src/setroubleshoot/rpc.py b/src/setroubleshoot/rpc.py +index aca2d7e..6a6f31a 100755 +--- a/src/setroubleshoot/rpc.py ++++ b/src/setroubleshoot/rpc.py +@@ -869,10 +869,17 @@ class RpcChannel(ConnectionIO, RpcManage): + result_code, result_msg) + self.io_watch_remove() + +- def get_method_implementation(self, interface, method): ++ def get_method_implementation(self, interface, method, expected_type=None): + handler_obj = self.rpc_handlers.get(interface, None) + if handler_obj is None: + return None ++ if expected_type is not None: ++ interface_dict = interface_registry.interfaces.get(interface) ++ if interface_dict is None: ++ return None ++ rpc_def = interface_dict.get(method) ++ if rpc_def is None or rpc_def.type != expected_type: ++ return None + method_ptr = getattr(handler_obj, method, None) + return method_ptr + +@@ -971,7 +978,7 @@ class RpcChannel(ConnectionIO, RpcManage): + self.handle_return(type, rpc_id, body) + elif type == 'method': + interface, method, args = convert_rpc_xml_to_args(body) +- method_ptr = self.get_method_implementation(interface, method) ++ method_ptr = self.get_method_implementation(interface, method, 'method') + if method_ptr: + try: + return_args = method_ptr(*args) +@@ -990,7 +997,7 @@ class RpcChannel(ConnectionIO, RpcManage): + self.emit_rpc(rpc_id, 'error_return', rpc_error_def, method, err_code, err_msg) + elif type == 'signal': + interface, method, args = convert_rpc_xml_to_args(body) +- method_ptr = self.get_method_implementation(interface, method) ++ method_ptr = self.get_method_implementation(interface, method, 'signal') + if method_ptr: + try: + method_ptr(*args) +-- +2.53.0 + diff --git a/0004-Reject-logon-when-peer-credentials-are-unavailable.patch b/0004-Reject-logon-when-peer-credentials-are-unavailable.patch new file mode 100644 index 0000000..f56b7c6 --- /dev/null +++ b/0004-Reject-logon-when-peer-credentials-are-unavailable.patch @@ -0,0 +1,34 @@ +From 920ca71562e462ed4d1d02f3ce1b35dec33bd1bb Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Fri, 7 Aug 2026 14:57:21 +0200 +Subject: [PATCH] Reject logon() when peer credentials are unavailable + +get_credentials() returns uid=None for non-Unix client sockets (e.g. an +INET/TCP listener). logon() then compared the supplied username against +get_identity(None), which falls back to os.getuid() -- the daemon's own +uid -- letting any caller authenticate as the 'setroubleshoot' account +without a valid password on such a listener. Refuse logon outright when +peer credentials could not be obtained. + +Co-Authored-By: Claude Sonnet 5 +--- + src/setroubleshoot/server.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/setroubleshoot/server.py b/src/setroubleshoot/server.py +index cf0f345..bc38fa3 100755 +--- a/src/setroubleshoot/server.py ++++ b/src/setroubleshoot/server.py +@@ -324,6 +324,9 @@ class SetroubleshootdClientConnectionHandler(ClientConnectionHandler, + def logon(self, type, username, password): + log_debug("logon(%s) type=%s username=%s" % (self, type, username)) + ++ if self.uid is None: ++ raise ProgramError(ERR_USER_LOOKUP, detail="peer credentials unavailable; refusing logon on non-unix socket") ++ + if username != get_identity(self.uid): + raise ProgramError(ERR_USER_LOOKUP, detail="uid=%s does not match logon username (%s)" % (self.uid, username)) + +-- +2.53.0 + diff --git a/0005-Fix-port-handling.patch b/0005-Fix-port-handling.patch new file mode 100644 index 0000000..b3be645 --- /dev/null +++ b/0005-Fix-port-handling.patch @@ -0,0 +1,73 @@ +From 49f3d79ff621e7cf3876cea759ded665cd192528 Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Fri, 7 Aug 2026 14:58:20 +0200 +Subject: [PATCH] Fix port handling + +- Cast parsed port to int in SocketAddress.parse_inet_addr() + +An explicit port in an {inet} address (e.g. "{inet}127.0.0.1:16983") was +kept as the raw regex-matched string instead of being converted to int. +socket.bind() requires an int port for AF_INET, so any configured +INET/TCP listener with an explicit port failed with TypeError, silently +swallowed by ListeningServer.open()'s exception handler, leaving the +daemon running with no client listener at all. + +- Forward the configured default port in get_socket_list_from_config() + +parse_socket_address_list() defaults its default_port parameter to None, +and get_socket_list_from_config() called it without passing one through, +so any {inet} address with no explicit port (e.g. "{inet}127.0.0.1") ended +up with SocketAddress.port = None instead of the configured +connection.default_port, causing socket.bind() to fail with TypeError. + +- Fix invalid default connection.default_port value + +The shipped default was '69783' with a comment acknowledging it was a +placeholder ("FIXME: figure out defined port"). 69783 exceeds the valid +TCP port range (0-65535), so any {inet} listen_for_client/client_connect_to +address with no explicit port failed with OverflowError on bind()/connect(). + +Co-Authored-By: Claude Sonnet 5 +--- + src/config.py.in | 2 +- + src/setroubleshoot/rpc.py | 4 +++- + 2 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/src/config.py.in b/src/config.py.in +index d8e7e35..603a107 100644 +--- a/src/config.py.in ++++ b/src/config.py.in +@@ -182,7 +182,7 @@ An empty string implies no limit''', + }, + 'connection': { + 'default_port': { +- 'value': '69783', # FIXME: figure out defined port, ++ 'value': '16983', + 'description': '', + }, + }, +diff --git a/src/setroubleshoot/rpc.py b/src/setroubleshoot/rpc.py +index 6a6f31a..142228f 100755 +--- a/src/setroubleshoot/rpc.py ++++ b/src/setroubleshoot/rpc.py +@@ -100,7 +100,7 @@ def get_default_port(): + + def get_socket_list_from_config(cfg_section): + addr_string = get_config(cfg_section, 'address_list') +- socket_addresses = parse_socket_address_list(addr_string) ++ socket_addresses = parse_socket_address_list(addr_string, get_default_port()) + return socket_addresses + + +@@ -577,6 +577,8 @@ class SocketAddress(object): + port = match.group(3) + if port is None: + port = self.default_port ++ else: ++ port = int(port) + + if addr == 'hostname': + addr = get_hostname() +-- +2.53.0 + diff --git a/0006-Require-root-to-delete-alerts-via-D-Bus.patch b/0006-Require-root-to-delete-alerts-via-D-Bus.patch new file mode 100644 index 0000000..8d025d8 --- /dev/null +++ b/0006-Require-root-to-delete-alerts-via-D-Bus.patch @@ -0,0 +1,50 @@ +From 66942f3b55339e59f768952312548e7eab3d19ca Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Fri, 7 Aug 2026 17:44:46 +0200 +Subject: [PATCH] Require root to delete alerts via D-Bus + +delete_alert() removed an alert from the shared host database without +checking the caller's identity, and the shipped D-Bus policy allowed any +local user to invoke it. An unprivileged local user could enumerate alerts +with get_all_alerts and delete entries other users/administrators rely on. + +Reject non-root callers in delete_alert() itself, and drop delete_alert +from the default D-Bus policy context as defense in depth (root already +has full access via the existing rule). + +Co-Authored-By: Claude Sonnet 5 +--- + org.fedoraproject.Setroubleshootd.conf | 3 --- + src/setroubleshoot/server.py | 2 ++ + 2 files changed, 2 insertions(+), 3 deletions(-) + +diff --git a/org.fedoraproject.Setroubleshootd.conf b/org.fedoraproject.Setroubleshootd.conf +index 65a0daa..910958a 100644 +--- a/org.fedoraproject.Setroubleshootd.conf ++++ b/org.fedoraproject.Setroubleshootd.conf +@@ -35,9 +35,6 @@ + +- + + +diff --git a/src/setroubleshoot/server.py b/src/setroubleshoot/server.py +index bc38fa3..dc29f63 100755 +--- a/src/setroubleshoot/server.py ++++ b/src/setroubleshoot/server.py +@@ -665,6 +665,8 @@ Deletes an alert from the database. + + * `success(b)`: + """ ++ if self.connection.get_unix_user(sender) != 0: ++ return False + try: + database = get_host_database() + alert = self._get_alert(local_id, database) +-- +2.53.0 + diff --git a/0007-Protect-against-malicious-socket-blocking.patch b/0007-Protect-against-malicious-socket-blocking.patch new file mode 100644 index 0000000..6ddc0ee --- /dev/null +++ b/0007-Protect-against-malicious-socket-blocking.patch @@ -0,0 +1,55 @@ +From c30163055995cff80222899a35722819d8286417 Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Fri, 7 Aug 2026 18:03:16 +0200 +Subject: [PATCH] Protect against malicious socket blocking + +Set a timeout (socket.timeout config, 5s default) on accepted client +sockets so a stuck send() eventually times out instead of hanging forever +(send_data() already had a Socket.timeout handler for this). As a second, +independent layer, skip clients that have not completed logon() in both +places that fan data out to the 'sealert' pool: send_alert_notification() +and ClientNotifier.signatures_updated(). + +Co-Authored-By: Claude Sonnet 5 +--- + src/setroubleshoot/rpc.py | 1 + + src/setroubleshoot/server.py | 4 ++++ + 2 files changed, 5 insertions(+) + +diff --git a/src/setroubleshoot/rpc.py b/src/setroubleshoot/rpc.py +index 142228f..ecfd062 100755 +--- a/src/setroubleshoot/rpc.py ++++ b/src/setroubleshoot/rpc.py +@@ -687,6 +687,7 @@ class ListeningServer(ConnectionIO): + try: + client_socket, client_address = socket.accept() + fcntl.fcntl(client_socket.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) ++ client_socket.settimeout(RpcChannel.socket_timeout) + client_handler = self.client_connection_handler_class(self.socket_address) + client_handler.open(client_socket, client_address) + self.connection_state.update(0, ConnectionState.PROBLEM_FLAGS) +diff --git a/src/setroubleshoot/server.py b/src/setroubleshoot/server.py +index dc29f63..6adc0c2 100755 +--- a/src/setroubleshoot/server.py ++++ b/src/setroubleshoot/server.py +@@ -149,6 +149,8 @@ def send_alert_notification(siginfo): + system_bus.send_message(alert) + + for client in connection_pool.clients('sealert'): ++ if not (client.connection_state.flags & ConnectionState.AUTHENTICATED): ++ continue + client.alert(siginfo) + + #------------------------------ Variables ------------------------------- +@@ -445,6 +447,8 @@ class ClientNotifier(object): + + def signatures_updated(self, type, item): + for client in self.connection_pool.clients('sealert'): ++ if not (client.connection_state.flags & ConnectionState.AUTHENTICATED): ++ continue + client.signatures_updated(type, item) + + +-- +2.53.0 + diff --git a/0008-Require-privileged-access-to-change-email-alert-reci.patch b/0008-Require-privileged-access-to-change-email-alert-reci.patch new file mode 100644 index 0000000..f6dce69 --- /dev/null +++ b/0008-Require-privileged-access-to-change-email-alert-reci.patch @@ -0,0 +1,38 @@ +From 271fcfc09a53fba13b0e31df18acdc5d2605cf05 Mon Sep 17 00:00:00 2001 +From: Vit Mojzis +Date: Fri, 7 Aug 2026 18:05:56 +0200 +Subject: [PATCH] Require privileged access to change email alert recipients + +set_email_recipients() only checked that the RPC connection was +authenticated, not that the caller was privileged. Since logon() grants +authenticated state to any local user allowed to run the sealert client +(client_users='*' by default) and never verifies the supplied password, +any such user could overwrite the daemon-wide email_alert_recipients file +that controls where SELinux alert emails are sent. + +Reuse the existing fix_cmd privilege model (already used to gate running +alert fix commands as root) to restrict this to root and users listed in +fix_cmd_users. + +Co-Authored-By: Claude Sonnet 5 +--- + src/setroubleshoot/server.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/setroubleshoot/server.py b/src/setroubleshoot/server.py +index 6adc0c2..bf7a294 100755 +--- a/src/setroubleshoot/server.py ++++ b/src/setroubleshoot/server.py +@@ -364,6 +364,9 @@ class SetroubleshootdClientConnectionHandler(ClientConnectionHandler, + if not (self.connection_state.flags & ConnectionState.AUTHENTICATED): + raise ProgramError(ERR_NOT_AUTHENTICATED) + ++ if self.uid != 0 and not self.access.user_allowed('fix_cmd', self.username): ++ raise ProgramError(ERR_USER_PROHIBITED) ++ + email_recipients = recipients + email_recipients.write_recipient_file(email_recipients_filepath) + +-- +2.53.0 + diff --git a/setroubleshoot.spec b/setroubleshoot.spec index 9d65143..a1ad19c 100644 --- a/setroubleshoot.spec +++ b/setroubleshoot.spec @@ -6,9 +6,10 @@ Summary: Helps troubleshoot SELinux problems Name: setroubleshoot Version: 3.3.37 -Release: 5%{?dist} +Release: 6%{?dist} License: GPL-2.0-or-later URL: https://gitlab.com/setroubleshoot/setroubleshoot +VCS: git:https://gitlab.com/setroubleshoot/setroubleshoot.git Source0: https://gitlab.com/-/project/24478376/uploads/cbdfc2a87b350583c32b168fd9aad9fd/setroubleshoot-3.3.37.tar.gz Source1: %{name}.tmpfiles Source2: %{name}.sysusers @@ -16,6 +17,12 @@ Source2: %{name}.sysusers # for j in 00*patch; do printf "Patch: %s\n" $j; done Patch: 0001-Update-GPL2-license-texts-to-the-latest-version.patch Patch: 0002-Limit-RPC-request-size-in-RequestReceiver-to-prevent.patch +Patch: 0003-Restrict-RPC-dispatch-to-registered-methods-only.patch +Patch: 0004-Reject-logon-when-peer-credentials-are-unavailable.patch +Patch: 0005-Fix-port-handling.patch +Patch: 0006-Require-root-to-delete-alerts-via-D-Bus.patch +Patch: 0007-Protect-against-malicious-socket-blocking.patch +Patch: 0008-Require-privileged-access-to-change-email-alert-reci.patch BuildRequires: gcc BuildRequires: make BuildRequires: libcap-ng-devel @@ -194,6 +201,14 @@ to user preference. The same tools can be run on existing log files. %doc AUTHORS COPYING ChangeLog DBUS.md NEWS README TODO %changelog +* Fri Aug 07 2026 Vit Mojzis - 3.3.37-6 +- Require privileged access to change email alert recipients +- Protect against malicious socket blocking +- Require root to delete alerts via D-Bus +- Fix port handling +- Reject logon() when peer credentials are unavailable +- Restrict RPC dispatch to registered methods only + * Wed Jul 29 2026 Vit Mojzis - 3.3.37-5 - Update GPL2 license texts to the latest version - Limit RPC request size in RequestReceiver to prevent memory exhaustion