CVE-2021-21367: Incorrect Authorization (CWE-863)
value: Switchboard Bluetooth Plug for elementary OS from version 2.3.0 and before version version 2.3.5 has an incorrect authorization vulnerability. When the Bluetooth plug is running (in discoverable mode), Bluetooth service requests and pairing requests are automatically accepted, allowing physically proximate attackers to pair with a device running an affected version of switchboard-plug-bluetooth without the active consent of the user. By default, elementary OS doesn't expose any services via Bluetooth that allow information to be extracted by paired Bluetooth devices. However, if such services (i.e. contact list sharing software) have been installed, it's possible that attackers have been able to extract data from such services without authorization. If no such services have been installed, attackers are only able to pair with a device running an affected version without authorization and then play audio out of the device or possibly present a HID device (keyboard, mouse, etc...) to control the device. As such, users should check the list of trusted/paired devices and remove any that are not 100% confirmed to be genuine. This is fixed in version 2.3.5. To reduce the likelihood of this vulnerability on an unpatched version, only open the Bluetooth plug for short intervals when absolutely necessary and preferably not in crowded public areas. To mitigate the risk entirely with unpatched versions, do not open the Bluetooth plug within switchboard at all, and use a different method for pairing devices if necessary (e.g. `bluetoothctl` CLI).
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 8.1
Published: 2021-03-12T17:15Z
Repository: https://github.com/elementary/switchboard-plug-bluetooth
Language: Other
Files changed: src/PairDialog.vala, src/Services/Agent.vala
Commit date: 2021-03-09 10:24:07 -0700
Commit message: Merge pull request from GHSA-5p3g-j69g-w2mq

* Implement unimplemented methods

* Check the dialog response

* Support cancellation

* Add some clarifying comments

* Couple more comments
Diff: diff --git a/src/PairDialog.vala b/src/PairDialog.vala
index c5e42d3..ff6950f 100644
--- a/src/PairDialog.vala
+++ b/src/PairDialog.vala
@@ -24,10 +24,11 @@ public class PairDialog : Granite.MessageDialog {
     }
 
     public ObjectPath object_path { get; construct; }
     public AuthType auth_type { get; construct; }
     public string passkey { get; construct; }
+    public bool cancelled { get; set; }
 
     // Un-used default constructor
     private PairDialog () {
         Object (
             buttons: Gtk.ButtonsType.CANCEL
@@ -91,17 +92,21 @@ public class PairDialog : Granite.MessageDialog {
 
         switch (auth_type) {
             case AuthType.REQUEST_CONFIRMATION:
                 badge_icon = new ThemedIcon ("dialog-password");
                 secondary_text = _("Make sure the code displayed on “%s” matches the one below.").printf (device_name);
+
+                var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
+                confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
                 break;
             case AuthType.DISPLAY_PASSKEY:
                 badge_icon = new ThemedIcon ("dialog-password");
                 secondary_text = _("“%s” would like to pair with this device. Make sure the code displayed on “%s” matches the one below.").printf (device_name, device_name);
 
                 var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
                 confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
+                break;
             case AuthType.DISPLAY_PIN_CODE:
                 badge_icon = new ThemedIcon ("dialog-password");
                 secondary_text = _("Type the code displayed below on “%s”, followed by Enter.").printf (device_name);
                 break;
             case AuthType.REQUEST_AUTHORIZATION:
@@ -120,25 +125,7 @@ public class PairDialog : Granite.MessageDialog {
             custom_bin.add (passkey_label);
             custom_bin.show_all ();
         }
 
         modal = true;
-
-        response.connect ((response_id) => {
-            switch (response_id) {
-                case Gtk.ResponseType.ACCEPT:
-                    device.pair.begin ();
-                    break;
-                case Gtk.ResponseType.CANCEL:
-                    destroy ();
-                    break;
-            }
-        });
-
-        ((DBusProxy)device).g_properties_changed.connect ((changed, invalid) => {
-            var paired = changed.lookup_value ("Paired", new VariantType ("b"));
-            if (paired != null && device.paired) {
-                destroy ();
-            }
-        });
     }
 }
diff --git a/src/Services/Agent.vala b/src/Services/Agent.vala
index 235d1a5..1776243 100644
--- a/src/Services/Agent.vala
+++ b/src/Services/Agent.vala
@@ -26,10 +26,12 @@ public errordomain BluezError {
 [DBus (name = "org.bluez.Agent1")]
 public class Bluetooth.Services.Agent : Object {
     private const string PATH = "/org/bluez/agent/elementary";
     Gtk.Window? main_window;
 
+    private PairDialog pair_dialog;
+
     [DBus (visible=false)]
     public Agent (Gtk.Window? main_window) {
         this.main_window = main_window;
         Bus.own_name (BusType.SYSTEM, "org.bluez.AgentManager1", BusNameOwnerFlags.NONE,
             (connection, name) => {
@@ -58,39 +60,106 @@ public class Bluetooth.Services.Agent : Object {
 
     public void release () throws Error {
         unregistered ();
     }
 
-    public string request_pin_code (ObjectPath device) throws Error, BluezError {
-        return "";
+    public async string request_pin_code (ObjectPath device) throws Error, BluezError {
+        throw new BluezError.REJECTED ("Pairing method not supported");
     }
 
-    public void display_pin_code (ObjectPath device, string pincode) throws Error, BluezError {
-        var pair_dialog = new PairDialog.display_pin_code (device, pincode, main_window);
+    // Called to display a pin code on-screen that needs to be entered on the other device. Can return
+    // instantly
+    public async void display_pin_code (ObjectPath device, string pincode) throws Error, BluezError {
+        pair_dialog = new PairDialog.display_pin_code (device, pincode, main_window);
         pair_dialog.present ();
     }
 
-    public uint32 request_passkey (ObjectPath device) throws Error, BluezError {
-        return 0;
+    public async uint32 request_passkey (ObjectPath device) throws Error, BluezError {
+        throw new BluezError.REJECTED ("Pairing method not supported");
     }
 
-    public void display_passkey (ObjectPath device, uint32 passkey, uint16 entered) throws Error {
-        var pair_dialog = new PairDialog.display_passkey (device, passkey, entered, main_window);
+    // Called to display a passkey on-screen that needs to be entered on the other device. Can return
+    // instantly
+    public async void display_passkey (ObjectPath device, uint32 passkey, uint16 entered) throws Error {
+        pair_dialog = new PairDialog.display_passkey (device, passkey, entered, main_window);
         pair_dialog.present ();
     }
 
-    public void request_confirmation (ObjectPath device, uint32 passkey) throws Error, BluezError {
-        var pair_dialog = new PairDialog.request_confirmation (device, passkey, main_window);
-        pair_dialog.present ();
+    // Called to request confirmation from the user that they want to pair with the given device and that
+    // the passkey matches. **MUST** throw BluezError if pairing is to be rejected. This is handled in
+    // `check_pairing_response`. If the method returns without an error, pairing is authorized
+    public async void request_confirmation (ObjectPath device, uint32 passkey) throws Error, BluezError {
+        pair_dialog = new PairDialog.request_confirmation (device, passkey, main_window);
+        yield check_pairing_response (pair_dialog);
     }
 
-    public void request_authorization (ObjectPath device) throws Error, BluezError {
-        var pair_dialog = new PairDialog.request_authorization (device, main_window);
-        pair_dialog.present ();
+    // Called to request confirmation from the user that they want to pair with the given device
+    // **MUST** throw BluezError if pairing is to be rejected. This is handled in `check_pairing_response`
+    // If the method returns without an error, pairing is authorized
+    public async void request_authorization (ObjectPath device) throws Error, BluezError {
+        pair_dialog = new PairDialog.request_authorization (device, main_window);
+        yield check_pairing_response (pair_dialog);
     }
 
-    public void authorize_service (ObjectPath device, string uuid) throws Error, BluezError {
+    // Called to authorize the use of a specific service (Audio/HID/etc), so we restrict this to paired
+    // devices only
+    public void authorize_service (ObjectPath device_path, string uuid) throws Error, BluezError {
+        var device = get_device_from_object_path (device_path);
+
+        bool paired = device.paired;
+        bool trusted = device.trusted;
+
+        // Shouldn't really happen as trusted devices should be automatically authorized, but lets handle it anyway
+        if (paired && trusted) {
+            // allow
+            return;
+        }
+
+        // A device that has been paired, but not yet trusted, trust it and allow it to access
+        // services
+        if (paired && !trusted) {
+            device.trusted = true;
+            // allow
+            return;
+        }
+
+        // Reject everything else
+        throw new BluezError.REJECTED ("Rejecting service auth, not paired or trusted");
     }
 
     public void cancel () throws Error {
+        if (pair_dialog != null) {
+            pair_dialog.cancelled = true;
+            pair_dialog.destroy ();
+        }
+    }
+
+    private async void check_pairing_response (PairDialog dialog) throws BluezError {
+        SourceFunc callback = check_pairing_response.callback;
+        BluezError? error = null;
+
+        dialog.response.connect ((response) => {
+            if (response != Gtk.ResponseType.ACCEPT || dialog.cancelled) {
+                if (dialog.cancelled) {
+                    error = new BluezError.CANCELED ("Pairing cancelled");
+                } else {
+                    error = new BluezError.REJECTED ("Pairing rejected");
+                }
+            }
+
+            Idle.add ((owned)callback);
+            dialog.destroy ();
+        });
+
+        dialog.present ();
+
+        yield;
+
+        if (error != null) {
+            throw error;
+        }
+    }
+
+    private Device get_device_from_object_path (ObjectPath object_path) throws GLib.Error {
+        return Bus.get_proxy_sync<Device> (BusType.SYSTEM, "org.bluez", object_path, DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
     }
 }

Vulnerable code: 
        response.connect ((response_id) => {
            switch (response_id) {
                case Gtk.ResponseType.ACCEPT:
                    device.pair.begin ();
                    break;
                case Gtk.ResponseType.CANCEL:
                    destroy ();
                    break;
            }
        });

        ((DBusProxy)device).g_properties_changed.connect ((changed, invalid) => {
            var paired = changed.lookup_value ("Paired", new VariantType ("b"));
            if (paired != null && device.paired) {
                destroy ();
            }
        });
    public string request_pin_code (ObjectPath device) throws Error, BluezError {
        return "";
    public void display_pin_code (ObjectPath device, string pincode) throws Error, BluezError {
        var pair_dialog = new PairDialog.display_pin_code (device, pincode, main_window);
    public uint32 request_passkey (ObjectPath device) throws Error, BluezError {
        return 0;
    public void display_passkey (ObjectPath device, uint32 passkey, uint16 entered) throws Error {
        var pair_dialog = new PairDialog.display_passkey (device, passkey, entered, main_window);
    public void request_confirmation (ObjectPath device, uint32 passkey) throws Error, BluezError {
        var pair_dialog = new PairDialog.request_confirmation (device, passkey, main_window);
        pair_dialog.present ();
    public void request_authorization (ObjectPath device) throws Error, BluezError {
        var pair_dialog = new PairDialog.request_authorization (device, main_window);
        pair_dialog.present ();
    public void authorize_service (ObjectPath device, string uuid) throws Error, BluezError {
Fixed code:     public bool cancelled { get; set; }

                var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
                confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
                break;
    private PairDialog pair_dialog;

    public async string request_pin_code (ObjectPath device) throws Error, BluezError {
        throw new BluezError.REJECTED ("Pairing method not supported");
    // Called to display a pin code on-screen that needs to be entered on the other device. Can return
    // instantly
    public async void display_pin_code (ObjectPath device, string pincode) throws Error, BluezError {
        pair_dialog = new PairDialog.display_pin_code (device, pincode, main_window);
    public async uint32 request_passkey (ObjectPath device) throws Error, BluezError {
        throw new BluezError.REJECTED ("Pairing method not supported");
    // Called to display a passkey on-screen that needs to be entered on the other device. Can return
    // instantly
    public async void display_passkey (ObjectPath device, uint32 passkey, uint16 entered) throws Error {
        pair_dialog = new PairDialog.display_passkey (device, passkey, entered, main_window);
    // Called to request confirmation from the user that they want to pair with the given device and that
    // the passkey matches. **MUST** throw BluezError if pairing is to be rejected. This is handled in
    // `check_pairing_response`. If the method returns without an error, pairing is authorized
    public async void request_confirmation (ObjectPath device, uint32 passkey) throws Error, BluezError {
        pair_dialog = new PairDialog.request_confirmation (device, passkey, main_window);
        yield check_pairing_response (pair_dialog);
    // Called to request confirmation from the user that they want to pair with the given device
    // **MUST** throw BluezError if pairing is to be rejected. This is handled in `check_pairing_response`
    // If the method returns without an error, pairing is authorized
    public async void request_authorization (ObjectPath device) throws Error, BluezError {
        pair_dialog = new PairDialog.request_authorization (device, main_window);
        yield check_pairing_response (pair_dialog);
    // Called to authorize the use of a specific service (Audio/HID/etc), so we restrict this to paired
    // devices only
    public void authorize_service (ObjectPath device_path, string uuid) throws Error, BluezError {
        var device = get_device_from_object_path (device_path);

        bool paired = device.paired;
        bool trusted = device.trusted;

        // Shouldn't really happen as trusted devices should be automatically authorized, but lets handle it anyway
        if (paired && trusted) {
            // allow
            return;
        }

        // A device that has been paired, but not yet trusted, trust it and allow it to access
        // services
        if (paired && !trusted) {
            device.trusted = true;
            // allow
            return;
        }

        // Reject everything else
        throw new BluezError.REJECTED ("Rejecting service auth, not paired or trusted");
        if (pair_dialog != null) {
            pair_dialog.cancelled = true;
            pair_dialog.destroy ();
        }
    }

    private async void check_pairing_response (PairDialog dialog) throws BluezError {
        SourceFunc callback = check_pairing_response.callback;
        BluezError? error = null;

        dialog.response.connect ((response) => {
            if (response != Gtk.ResponseType.ACCEPT || dialog.cancelled) {
                if (dialog.cancelled) {
                    error = new BluezError.CANCELED ("Pairing cancelled");
                } else {
                    error = new BluezError.REJECTED ("Pairing rejected");
                }
            }

            Idle.add ((owned)callback);
            dialog.destroy ();
        });

        dialog.present ();

        yield;

        if (error != null) {
            throw error;
        }
    }

    private Device get_device_from_object_path (ObjectPath object_path) throws GLib.Error {
        return Bus.get_proxy_sync<Device> (BusType.SYSTEM, "org.bluez", object_path, DBusProxyFlags.GET_INVALIDATED_PROPERTIES);