CVE-2021-21367 (published 2021-03-12T17:15Z)

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).

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:A/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
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.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/elementary/switchboard-plug-bluetooth
commit_details: repo_url: https://github.com/elementary/switchboard-plug-bluetooth
author: David Hewitt
author_date: 2021-03-09 17:24:07+00:00
msg: Merge pull request from GHSA-5p3g-j69g-w2mq

* Implement unimplemented methods

* Check the dialog response

* Support cancellation

* Add some clarifying comments

* Couple more comments
repository: repo_url: https://github.com/elementary/switchboard-plug-bluetooth
repo_name: elementary/switchboard-plug-bluetooth
description: Switchboard Bluetooth Plug
repo_language: Vala
owner: elementary
file_changes: filename: PairDialog.vala
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ 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 () {
@@ -93,6 +94,9 @@ public class PairDialog : Granite.MessageDialog {
             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");
@@ -100,6 +104,7 @@ public class PairDialog : Granite.MessageDialog {
 
                 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);
@@ -122,23 +127,5 @@ public class PairDialog : Granite.MessageDialog {
         }
 
         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 ();
-            }
-        });
     }
 }

programming_language: C++
filename: Agent.vala
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,8 @@ 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;
@@ -60,37 +62,104 @@ public class Bluetooth.Services.Agent : Object {
         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);
     }
 }

programming_language: Java