-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RHEL-15110: Fix issue with registration using gsd-subman #3350
Conversation
jirihnidek
commented
Oct 31, 2023
- We were too agresive, when we fixed CVE in this PR: 2225446: Hotfix of D-Bus policy #3317
- It is still safe to allow non-root user to create abstract socket using Start() on interface com.redhat.RHSM1.RegisterServer and destroy it later using Stop(). This abstract socket could be later used by root user for calling e.g. Register() on interface com.redhat.RHSM1.Register. This is way how it works for gsd-subman (run by non-root user) and gsd-subman-helper (run by root user).
Coverage (computed on Fedora latest) •
|
Link to issue: https://issues.redhat.com/browse/RHELMISC-1500 |
I spent some time reviewing the impact of this change:
I think the patch is correct as is. If there was serious concern about allowing the |
I can confirm that I wasn't able to connect to abstract socket created by |
I think it still might be a good idea to add a check of some sort on the user for start and stop... It's not critical, but if anything it at least might fend off a DoS CVE report in N months from an overzealous security researcher or something... maybe something like class RegisterDBusObject(base_object.BaseObject):
default_dbus_path = constants.REGISTER_DBUS_PATH
interface_name = constants.REGISTER_INTERFACE
def __init__(self, conn=None, object_path=None, bus_name=None):
super().__init__(conn=conn, object_path=object_path, bus_name=bus_name)
self.impl = RegisterDBusImplementation()
+ self.caller_uids = set()
+ self.bus_proxy = self.conn.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
+ self.bus_interface = dbus.Interface(self.bus_proxy, 'org.freedesktop.DBus')
@util.dbus_service_method(
constants.REGISTER_INTERFACE,
in_signature="s",
out_signature="s",
)
@util.dbus_handle_sender
@util.dbus_handle_exceptions
def Start(self, locale, sender=None):
locale = dbus_utils.dbus_to_python(locale, expected_type=str)
Locale.set(locale)
+ caller_uid = self.bus_interface.GetConnectionUnixUser(sender)
+
address: str = self.impl.start(sender)
+
+ self.caller_uids.add(caller_uid)
+
return address
@util.dbus_service_method(
constants.REGISTER_INTERFACE,
in_signature="s",
out_signature="b",
)
@util.dbus_handle_sender
@util.dbus_handle_exceptions
def Stop(self, locale, sender=None):
locale = dbus_utils.dbus_to_python(locale, expected_type=str)
Locale.set(locale)
- self.impl.stop()
+ caller_uid = self.bus_interface.GetConnectionUnixUser(sender)
+
+ self.caller_uids.discard(caller_uid)
+
+ if not self.caller_uids or caller_uid == 0:
+ self.impl.stop()
(untested) |
Very good point. Only non-root user that called I will update PR. |
b7b92e5
to
ef7c263
Compare
src/rhsmlib/dbus/objects/register.py
Outdated
|
||
# When somebody else started domain socket listener, then | ||
if self.impl.server is not None: | ||
log.warning(f"domain socket listener already running, opened by: {self.impl.server.sender}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be better to include _caller_uid
here. it's probably a little more recognizable in a log than a d-bus unique name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, just to summarize a slack conversation between @jirihnidek @owtaylor @m-horky and me. This causes a few other problems:
- There's still the DoS problem because one user can call
Start()
and prevent another user from callingStart()
- If the user session crashes during login before
Stop()
is called, they'll have to reboot to register instead of just log back in - gnome-settings-daemon currently has unpaired
Start()
in its unregister path. So if user unregisters they'll have to reboot to register again
So I think Start()
needs to silently succeed if it's already started to maintain backward compatibility. The two easiest ways to do that are:
- go back to tracking a set of caller uids like the sketch from RHEL-15110: Fix issue with registration using gsd-subman #3350 (comment)
- Allow
Start()
from multiple users but makeStop()
a no-op unless it's called by root (as mentioned earlier in RHEL-15110: Fix issue with registration using gsd-subman #3350 (comment) )
ef7c263
to
a26529f
Compare
I reworked the code. Long story short: The policy should not be about users, but it should be about applications using |
BTW: I will have to fix some unit tests (probably add more mocks). Tests do not work, when you try to run these tests in container, because there is no system bus. |
As far as I can tell, the code seems to work as advertised. I'm still not sure that complicated tracking of Start/Stop is warranted. Can you explain a bit more about why you want do that? Experimentally, stopping the server saves only a tiny fraction of resident memory:
One thing that your approach definitely does improve compared to just leaving running is tracking of the "sender", which is used to retrieve the command line information. But it still isn't very reliable - any user could make it whatever they want by calling Start() without Stop() first. What about instead doing something like: From b6f401d70438f528c278e2e02ccc1c041da10c67 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <[email protected]>
Date: Fri, 3 Nov 2023 10:58:41 -0400
Subject: [PATCH] registration: get the sender command line from the peer
connection
It's hard to reliably associate Start/Stop methods with the calls to the
actual registration methods, so instead retrieve the PID from the
caller of the private method and look up the command line in /proc.
---
src/rhsmlib/client_info.py | 17 +++++++++++++++++
src/rhsmlib/dbus/objects/register.py | 10 ++++++----
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/src/rhsmlib/client_info.py b/src/rhsmlib/client_info.py
index a7132266d..981024e8f 100644
--- a/src/rhsmlib/client_info.py
+++ b/src/rhsmlib/client_info.py
@@ -74,6 +74,23 @@ class DBusSender:
self.cmd_line = cmd_line
log.debug("D-Bus sender: %s (cmd-line: %s)" % (sender, self.cmd_line))
+ def set_cmd_line_from_peer_connection(self, dbus_connection):
+ """
+ Set the sender's command line in the singleton object, by looking it up from /proc
+ """
+
+ peer_pid = dbus_connection.get_peer_unix_process_id()
+ if peer_pid is None:
+ log.debug("Can't get peer pid for connection")
+ return
+
+ try:
+ with open("/proc/%d/cmdline" % peer_pid, "rb") as f:
+ self.cmd_line = f.read().split(b"\0")[0].decode("utf-8")
+ log.debug("D-Bus sender pid: %d (cmd-line: %s)" % (peer_pid, self.cmd_line))
+ except (OSError, UnicodeDecodeError) as e:
+ log.debug("Can't read cmd-line for pid %d: %s" % (peer_pid, str(e)))
+
def reset_cmd_line(self):
"""
Reset sender's command line
diff --git a/src/rhsmlib/dbus/objects/register.py b/src/rhsmlib/dbus/objects/register.py
index c31151ddf..91b8df5f7 100644
--- a/src/rhsmlib/dbus/objects/register.py
+++ b/src/rhsmlib/dbus/objects/register.py
@@ -384,9 +384,10 @@ class DomainSocketRegisterDBusObject(base_object.BaseObject):
dbus_interface=constants.PRIVATE_REGISTER_INTERFACE,
in_signature="sssa{sv}a{sv}s",
out_signature="s",
+ connection_keyword="dbus_connection"
)
@util.dbus_handle_exceptions
- def Register(self, org, username, password, options, connection_options, locale):
+ def Register(self, org, username, password, options, connection_options, locale, dbus_connection):
"""
This method registers the system using basic auth
(username and password for a given org).
@@ -405,7 +406,7 @@ class DomainSocketRegisterDBusObject(base_object.BaseObject):
locale = dbus_utils.dbus_to_python(locale, expected_type=str)
with DBusSender() as dbus_sender:
- dbus_sender.set_cmd_line(sender=self.sender, cmd_line=self.cmd_line)
+ dbus_sender.set_cmd_line_from_peer_connection(dbus_connection)
Locale.set(locale)
consumer: dict = self.impl.register_with_credentials(org, options, connection_options)
@@ -416,9 +417,10 @@ class DomainSocketRegisterDBusObject(base_object.BaseObject):
dbus_interface=constants.PRIVATE_REGISTER_INTERFACE,
in_signature="sasa{sv}a{sv}s",
out_signature="s",
+ connection_keyword="dbus_connection"
)
@util.dbus_handle_exceptions
- def RegisterWithActivationKeys(self, org, activation_keys, options, connection_options, locale):
+ def RegisterWithActivationKeys(self, org, activation_keys, options, connection_options, locale, dbus_connection):
"""
Note this method is registration ONLY. Auto-attach is a separate process.
"""
@@ -429,7 +431,7 @@ class DomainSocketRegisterDBusObject(base_object.BaseObject):
locale = dbus_utils.dbus_to_python(locale, expected_type=str)
with DBusSender() as dbus_sender:
- dbus_sender.set_cmd_line(sender=self.sender, cmd_line=self.cmd_line)
+ dbus_sender.set_cmd_line_from_peer_connection(dbus_connection)
Locale.set(locale)
consumer: dict = self.impl.register_with_activation_keys(org, options, connection_options)
--
2.41.0 |
I try to minimize changes of functionality. I don't want to keep I don't think that it is necessary to get information about application calling BTW: we already have method called https://github.com/candlepin/subscription-manager/blob/main/src/rhsmlib/dbus/dbus_utils.py#L29 |
My first idea when reading your code was to suggest using the The functionality that clients depend upon is being able to get the server address and call the registration methods on it.
Yeah, the point is not that it's typically different, it's that when we start getting into the corner cases of Start/Stop, we don't reliably know how to connect Start calls to
Can you explain how my suggestion breaks backwards compat? I don't think it would require any changes to any callers. I guess you would start getting
Ah, that could simplify things a bit further. |
374b592
to
0f4b79d
Compare
Oh, I'm sorry. I overlooked that
I don't want to make more changes in this PR than necessary. |
Yea that was my first reaction too. Using
(in addition to discarding on It actually might be less code, since It's certainly more efficient, but maybe in practice it doesn't matter that much since in practice we're talking about a sender list of size 1, so iterating over the entire list isn't likely to be a problem.
In fact, that change would be fixing a bug right? It would be somewhat bad if a registration attempt got attributed to a process from a command from a different user for instance. It seems like this is another somewhat independent bug that should probably get fixed lest a security researcher files a CVE about it in M months, right? I do have to say after several rounds of changes of increasing complexity, though, my bear detector is starting to go off, and that again is making me like your Stop-is-a-nop approach better (I realize @jirihnidek isn't a fan of that, so I'm just giving my two cents). Of course, even with the Stop-is-a-nop approach, the auditing fix you mention above is still relevant. |
* We were too agresive, when we fixed CVE in this PR: #3317 * It is still safe to allow non-root user to create abstract socket using Start() on interface com.redhat.RHSM1.RegisterServer and destroy it later using Stop(). This abstract socket could be later used by root user for calling e.g. Register() on interface com.redhat.RHSM1.Register. This is way how it works for gsd-subman (run by non-root user) and gsd-subman-helper (run by root user).
* When some application starts RegisterServer using Start(), then it returns address. When this method is called multiple times and RegisterServer is still running, then the same address is returned. It means, when user starts multiple applications, then all of these applications should be able to use the same address to keep backward compatibility. The RegisterServer should be terminated only in the case, when the last application call Stop() and there is no running proccess using this RegisterServer. * This change does not allow to stop RegisterServer by some other user or application using Stop() method, when RegisterServer is still needed. * Non-root user can stop the RegisterServer only in the situation, when it is not needed by any application. * Implementation of Stop() method did not return anything explicitly, but the D-Bus method was designed to return boolean value. Thus, None object was interpreted as "False" value. It was fixed and it returns "True", when it was really stoped and it return "False", when it was not possible to stop it, because some application still uses RegisterServer. * It looks like that there was probably intention to use similar approach, but it has never been finished. * Modified some unit tests
0f4b79d
to
bc33776
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks folks for the code & the discussion on this!
LGTM, so merging it.
* Backport to 1.28 branch. Original PR: #3350 * Original commit: ba1e0e4 * We were too agresive, when we fixed CVE in this PR: #3318 * It is still safe to allow non-root user to create abstract socket using Start() on interface com.redhat.RHSM1.RegisterServer and destroy it later using Stop(). This abstract socket could be later used by root user for calling e.g. Register() on interface com.redhat.RHSM1.Register. This is way how it works for gsd-subman (run by non-root user) and gsd-subman-helper (run by root user).