Category: auth-bypass / information-disclosure
Severity: High
Primary location: app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt:285-294
Blast radius: BluetoothPacketBroadcaster.kt:428-448, BluetoothConnectionTracker.kt:176-179
Description
The BLE GATT server adds any device that writes the CCCD (ENABLE_NOTIFICATION_VALUE) to the
broadcast subscription list — no authentication, no Noise session, no verified ANNOUNCE, not even
a single prior packet from that device is required. Once subscribed, the device receives a live
feed of all broadcast mesh traffic via notifyCharacteristicChanged, plus relayed unicast
traffic (including encrypted DM ciphertext and its routing metadata). An attacker within BLE range
therefore passively harvests identities, social/topology data, public-channel plaintext, and
DM metadata with zero cryptographic work.
This is the transport-layer root of THREAT-MODEL threat T-01: "GATT notifications granted to any peer; passive receiver of identity + mesh graph."
code affected
// onDescriptorWriteRequest() — BluetoothGattServerManager.kt:280-299
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
connectionTracker.addSubscribedDevice(device) // :286 ← unconditional
connectionScope.launch {
delay(100)
if (isActive) {
delegate?.onDeviceConnected(device) // :292 ← peer-connect semantics
}
}
}Subscription bookkeeping accepts duplicates and has no auth gate:
// BluetoothConnectionTracker.kt:176-179
fun addSubscribedDevice(device: BluetoothDevice) {
subscribedDevices.add(device) // CopyOnWriteArrayList — no contains() check
}Every broadcast/relay path fans out to all subscribers:
// broadcastSinglePacketInternal() — BluetoothPacketBroadcaster.kt:428-448
val subscribedDevices = connectionTracker.getSubscribedDevices()
...
subscribedDevices.forEach { device ->
if (device.address == routed.relayAddress) return@forEach // only loop-suppression
if (connectionTracker.addressPeerMap[device.address] == senderID) return@forEach
val sent = notifyDevice(device, data, gattServer, characteristic)
...
}Remediation
something similar tosubscribed only after it participation is proved:
// onCharacteristicWriteRequest already gives us inbound packets per device.
// Track first valid signed ANNOUNCE per device address, then allow CCCD grant:
override fun onDescriptorWriteRequest(...) {
val eligible = connectionTracker.hasVerifiedAnnounce(device.address) // NEW
if (ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
if (!eligible) {
sendResponse(device, requestId, GATT_INSUFFICIENT_AUTHORIZATION, 0, null) // fail closed
return
}
connectionTracker.addSubscribedDeviceOnce(device) // dedup variant
...
} else if (DISABLE_NOTIFICATION_VALUE.contentEquals(value)) { // NEW unsubscribe
connectionTracker.removeSubscribedDevice(device)
}
...
}Activity
0No activity yet.