Bluetooth · BLE · Pairing

BLE Pairing Pitfalls: Just Works, KNOB and the Static Mistakes That Make Them Trivial

May 2, 2026 11 min read CVE-2019-9506 · LESC · GATT

BLE pairing is the moment a peripheral makes its only real cryptographic decision. Pick the wrong pairing mode and the rest of the security stack — the GATT permission flags, the bonding records, the LTK rotation — is decoration. This post is about the small set of source-level decisions that decide whether your BLE product is actually secure or merely looks secure on the data sheet.

The single most useful BLE security review question: "If an attacker is in range during pairing, what stops them from being a man-in-the-middle?" If the answer is "nothing, but it's a low-risk peripheral," say so explicitly. If the answer is "we use Just Works," on a peripheral that handles auth tokens or unlocks something physical, we have a finding.

The five pairing modes, ranked

BLE 4.2+ defines five association models. The capabilities of the two devices — the IO Capabilities field — determine which one gets used. The IO Capabilities setting in your code is therefore the most consequential single line in the BLE stack.

Just Works

Used when at least one side declares NoInputNoOutput. No MitM protection. The shared key is established but never confirmed, so an attacker in range during pairing can sit in the middle and pair with both sides.

Verdict: Acceptable only for genuinely non-sensitive peripherals (a temperature sensor that publishes a public reading).

Passkey Entry

One side displays a 6-digit passkey, the other side enters it. The passkey is mixed into the key agreement so a MitM cannot complete pairing without knowing it.

Verdict: Strong — but only if the passkey is randomly generated per pairing, not a static factory value.

Numeric Comparison (LESC only)

Both sides display a 6-digit number computed from the ECDH key agreement, the user confirms they match. Strongest standard mode short of OOB.

Verdict: Recommended for paired devices with displays on both ends.

Out-of-Band (OOB)

Pairing material is exchanged over a different channel (NFC tap, QR code, USB). MitM resistance depends on the OOB channel, but typically the strongest.

Verdict: Best for high-assurance products; least common in practice.

Legacy Pairing (BLE 4.0/4.1)

Pre-4.2 pairing using a temporary key. Susceptible to passive sniffing of the entire session if the temporary key is weak. Replaced by LESC and should not be used in any new design.

Verdict: Disable. Always.

The static red flags

1. NoInputNoOutput on a device that has a UI

This is the single most common BLE security mistake. A developer wants pairing to "just work" during prototyping, sets IO Capabilities to NoInputNoOutput, the device pairs with their phone immediately, and the setting never gets revisited.

anti-pattern
// ESP-IDF / NimBLE
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
ble_hs_cfg.sm_bonding = 1;
ble_hs_cfg.sm_mitm    = 0;        // no MitM!
ble_hs_cfg.sm_sc      = 0;        // no LESC!

Four lines, four problems. NO_INPUT_OUTPUT forces Just Works. sm_mitm = 0 does not require MitM protection. sm_sc = 0 disables LE Secure Connections, falling back to legacy pairing. And sm_bonding = 1 with all of the above means we will persist a weak key.

recommended
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;  // device shows passkey
ble_hs_cfg.sm_bonding = 1;
ble_hs_cfg.sm_mitm    = 1;        // require MitM protection
ble_hs_cfg.sm_sc      = 1;        // require LESC (BLE 4.2+)
ble_hs_cfg.sm_our_key_dist  = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;

2. LE Secure Connections (LESC) disabled

LESC was added in BLE 4.2 specifically to fix legacy pairing. Disabling it — or never enabling it — is shipping a 2010 cipher in a 2026 product. The static check is exact: if the BLE stack initialisation does not enable LESC, that is a finding. The names vary by stack:

3. Hardcoded passkeys and bonding keys

The point of Passkey Entry is that the passkey is generated fresh per pairing. A hardcoded 123456 in source defeats the protocol entirely:

anti-pattern
#define BLE_PASSKEY 123456
ble_sm_inject_io(conn_handle, BLE_PASSKEY);

// Or worse: a hardcoded long-term key
static const uint8_t bonding_ltk[16] = {
    0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x11, 0x22, 0x33,
    0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB
};

The same pattern in Java / Kotlin Android code (setPin() on BluetoothDevice), in Swift Core Bluetooth (custom passkey injection), in Python BlueZ helpers, and in Zigbee/BLE bridge code. Static analysis flags it whenever the value is a constant.

4. GATT characteristics without encryption / authentication

Pairing is one half. The other half is which characteristics require pairing to access. A common mistake is to add encryption requirements only to the obvious "secret" characteristic and leave the rest readable. Attackers don't read the obvious one; they read the firmware version characteristic to fingerprint the device, the configuration characteristic to learn the network topology, the diagnostics characteristic to find debug ports.

anti-pattern (NimBLE)
{
    .uuid       = BLE_UUID16_DECLARE(0xFF01),
    .access_cb  = read_config,
    .flags      = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
    // No _ENC, no _AUTHEN — readable / writable by anyone in range
}
recommended
{
    .uuid       = BLE_UUID16_DECLARE(0xFF01),
    .access_cb  = read_config,
    .flags      = BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN
                | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_WRITE_AUTHEN,
}

5. Permanent pairability + no whitelist

Some devices need to pair with anyone (a public health kiosk). Most don't. After commissioning, peripherals should drop into bonded-only mode — only accept connections from devices in the resolving list / whitelist. Code that calls advertising start with ADV_TYPE_IND and never transitions to ADV_TYPE_DIRECT_IND after first pairing is broadcasting "pair with me" forever.

KNOB: the attack the static check happens to also fix

KNOB (Key Negotiation of Bluetooth, CVE-2019-9506) is technically a Bluetooth Classic problem, but the lesson generalises. KNOB exploits a weakness in the BR/EDR encryption negotiation: an attacker in the pairing path can force the negotiated entropy down to 1 byte. With LESC properly enabled and minimum key length enforced in the stack configuration, this attack is mitigated. Static-analysis-wise: any BLE stack where you can grep for min_key_size = 7 (the spec minimum, not the practical minimum) is below par. min_key_size = 16 is the answer.

BleedingBit and BlueBorne: the chip-level reminder

Not every BLE vulnerability is in your code. BleedingBit (CVE-2018-16986/16987) was a stack-level OTA flaw in TI BLE chips. BlueBorne (2017) was a kernel-level Bluetooth stack flaw across Android, iOS, Linux and Windows. The lesson is dependency hygiene — treat your BLE stack vendor's CVE feed like an SCA source. SF365's SCA engine pulls vulnerabilities for embedded SDKs the same way it pulls them for npm.

The detection rules SF365 ships

The SF365 Wireless Security Engine ships five rules dedicated to BLE:

Each lands as a regular SF365 Finding with Category = "Wireless Security" and a BLE: title prefix, so they cluster together in dashboards, reports and the dedicated /wireless-security page.

Two checks worth adding to your CI today

Even without a wireless-aware scanner, two grep-level CI checks catch a non-trivial fraction of these issues:

Both will produce false positives. Both are still cheaper than the customer-disclosure call when a researcher pairs with your medical-device prototype at DEF CON.

Catch these BLE mistakes before pairing ships

SF365's Wireless Security Engine ships five BLE rules out of the box, mapped to a 24-control Wireless Security Baseline. No radios required — static analysis on the source you already have.

See the BLE Detections