Commit message: auth: Reject DBUS_COOKIE_SHA1 for users other than the server owner

The DBUS_COOKIE_SHA1 authentication mechanism aims to prove ownership
of a shared home directory by having the server write a secret "cookie"
into a .dbus-keyrings subdirectory of the desired identity's home
directory with 0700 permissions, and having the client prove that it can
read the cookie. This never actually worked for non-malicious clients in
the case where server uid != client uid (unless the server and client
both have privileges, such as Linux CAP_DAC_OVERRIDE or traditional
Unix uid 0) because an unprivileged server would fail to write out the
cookie, and an unprivileged client would be unable to read the resulting
file owned by the server.

Additionally, since dbus 1.7.10 we have checked that ~/.dbus-keyrings
is owned by the uid of the server (a side-effect of a check added to
harden our use of XDG_RUNTIME_DIR), further ruling out successful use
by a non-malicious client with a uid differing from the server's.

Joe Vennix of Apple Information Security discovered that the
implementation of DBUS_COOKIE_SHA1 was susceptible to a symbolic link
attack: a malicious client with write access to its own home directory
could manipulate a ~/.dbus-keyrings symlink to cause the DBusServer to
read and write in unintended locations. In the worst case this could
result in the DBusServer reusing a cookie that is known to the
malicious client, and treating that cookie as evidence that a subsequent
client connection came from an attacker-chosen uid, allowing
authentication bypass.

This is mitigated by the fact that by default, the well-known system
dbus-daemon (since 2003) and the well-known session dbus-daemon (in
stable releases since dbus 1.10.0 in 2015) only accept the EXTERNAL
authentication mechanism, and as a result will reject DBUS_COOKIE_SHA1
at an early stage, before manipulating cookies. As a result, this
vulnerability only applies to:

* system or session dbus-daemons with non-standard configuration
* third-party dbus-daemon invocations such as at-spi2-core (although
  in practice at-spi2-core also only accepts EXTERNAL by default)
* third-party uses of DBusServer such as the one in Upstart

Avoiding symlink attacks in a portable way is difficult, because APIs
like openat() and Linux /proc/self/fd are not universally available.
However, because DBUS_COOKIE_SHA1 already doesn't work in practice for
a non-matching uid, we can solve this vulnerability in an easier way
without regressions, by rejecting it early (before looking at
~/.dbus-keyrings) whenever the requested identity doesn't match the
identity of the process hosting the DBusServer.

Signed-off-by: Simon McVittie <smcv@collabora.com>
Closes: https://gitlab.freedesktop.org/dbus/dbus/issues/269
Closes: CVE-2019-12749
CVE: CVE-2019-12749
CWE: CWE-59
Vulnerability description: dbus before 1.10.28, 1.12.x before 1.12.16, and 1.13.x before 1.13.12, as used in DBusServer in Canonical Upstart in Ubuntu 14.04 (and in some, less common, uses of dbus-daemon), allows cookie spoofing because of symlink mishandling in the reference implementation of DBUS_COOKIE_SHA1 in the libdbus library. (This only affects the DBUS_COOKIE_SHA1 authentication mechanism.) A malicious client with write access to its own home directory could manipulate a ~/.dbus-keyrings symlink to cause a DBusServer with a different uid to read and write in unintended locations. In the worst case, this could result in the DBusServer reusing a cookie that is known to the malicious client, and treating that cookie as evidence that a subsequent client connection came from an attacker-chosen uid, allowing authentication bypass.

Project: dbus
File: dbus-auth.c

sha1_handle_first_client_response (DBusAuth         *auth,
                                   const DBusString *data)
{
  /* We haven't sent a challenge yet, we're expecting a desired
   * username from the client.
   */
  DBusString tmp;
  DBusString tmp2;
  dbus_bool_t retval = FALSE;
  DBusError error = DBUS_ERROR_INIT;

  _dbus_string_set_length (&auth->challenge, 0);
  
  if (_dbus_string_get_length (data) > 0)
    {
      if (_dbus_string_get_length (&auth->identity) > 0)
        {
          /* Tried to send two auth identities, wtf */
          _dbus_verbose ("%s: client tried to send auth identity, but we already have one\n",
                         DBUS_AUTH_NAME (auth));
          return send_rejected (auth);
        }
      else
        {
          /* this is our auth identity */
          if (!_dbus_string_copy (data, 0, &auth->identity, 0))
            return FALSE;
        }
    }
      
  if (!_dbus_credentials_add_from_user (auth->desired_identity, data))
    {
      _dbus_verbose ("%s: Did not get a valid username from client\n",
                     DBUS_AUTH_NAME (auth));
      return send_rejected (auth);
    }
      
  if (!_dbus_string_init (&tmp))
    return FALSE;

  if (!_dbus_string_init (&tmp2))
    {
      _dbus_string_free (&tmp);
      return FALSE;
    }

  /* we cache the keyring for speed, so here we drop it if it's the
   * wrong one. FIXME caching the keyring here is useless since we use
   * a different DBusAuth for every connection.
   */
  if (auth->keyring &&
      !_dbus_keyring_is_for_credentials (auth->keyring,
                                         auth->desired_identity))
    {
      _dbus_keyring_unref (auth->keyring);
      auth->keyring = NULL;
    }
  
  if (auth->keyring == NULL)
    {
      auth->keyring = _dbus_keyring_new_for_credentials (auth->desired_identity,
                                                         &auth->context,
                                                         &error);

      if (auth->keyring == NULL)
        {
          if (dbus_error_has_name (&error,
                                   DBUS_ERROR_NO_MEMORY))
            {
              dbus_error_free (&error);
              goto out;
            }
          else
            {
              _DBUS_ASSERT_ERROR_IS_SET (&error);
              _dbus_verbose ("%s: Error loading keyring: %s\n",
                             DBUS_AUTH_NAME (auth), error.message);
              if (send_rejected (auth))
                retval = TRUE; /* retval is only about mem */
              dbus_error_free (&error);
              goto out;
            }
        }
      else
        {
          _dbus_assert (!dbus_error_is_set (&error));
        }
    }

  _dbus_assert (auth->keyring != NULL);

  auth->cookie_id = _dbus_keyring_get_best_key (auth->keyring, &error);
  if (auth->cookie_id < 0)
    {
      _DBUS_ASSERT_ERROR_IS_SET (&error);
      _dbus_verbose ("%s: Could not get a cookie ID to send to client: %s\n",
                     DBUS_AUTH_NAME (auth), error.message);
      if (send_rejected (auth))
        retval = TRUE;
      dbus_error_free (&error);
      goto out;
    }
  else
    {
      _dbus_assert (!dbus_error_is_set (&error));
    }

  if (!_dbus_string_copy (&auth->context, 0,
                          &tmp2, _dbus_string_get_length (&tmp2)))
    goto out;

  if (!_dbus_string_append (&tmp2, " "))
    goto out;

  if (!_dbus_string_append_int (&tmp2, auth->cookie_id))
    goto out;

  if (!_dbus_string_append (&tmp2, " "))
    goto out;  
  
  if (!_dbus_generate_random_bytes (&tmp, N_CHALLENGE_BYTES, &error))
    {
      if (dbus_error_has_name (&error, DBUS_ERROR_NO_MEMORY))
        {
          dbus_error_free (&error);
          goto out;
        }
      else
        {
          _DBUS_ASSERT_ERROR_IS_SET (&error);
          _dbus_verbose ("%s: Error generating challenge: %s\n",
                         DBUS_AUTH_NAME (auth), error.message);
          if (send_rejected (auth))
            retval = TRUE; /* retval is only about mem */

          dbus_error_free (&error);
          goto out;
        }
    }

  _dbus_string_set_length (&auth->challenge, 0);
  if (!_dbus_string_hex_encode (&tmp, 0, &auth->challenge, 0))
    goto out;
  
  if (!_dbus_string_hex_encode (&tmp, 0, &tmp2,
                                _dbus_string_get_length (&tmp2)))
    goto out;

  if (!send_data (auth, &tmp2))
    goto out;
      
  goto_state (auth, &server_state_waiting_for_data);
  retval = TRUE;
  
 out:
  _dbus_string_zero (&tmp);
  _dbus_string_free (&tmp);
  _dbus_string_zero (&tmp2);
  _dbus_string_free (&tmp2);

  return retval;
}