Some bugs hide behind clever exploitation. This one is a memcpy with no length check, reachable before authentication, from any BLE radio in range.

TL;DR

CVE-2026-47773 (GHSA-77v6-cw9f-9whg). A missing bounds check in ArduinoBLE’s ATT write-request handler.

When a client writes to a characteristic that requires encryption (the BLEEncryption permission) but the link is not yet AES-encrypted, ArduinoBLE stashes the pending write into a fixed 64-byte writeBuffer inside the global ATTClass object, to replay it once encryption is up. It copies the attacker-supplied value into that buffer with no length check.

A remote, unauthenticated BLE client sends an oversized write and overflows the buffer with content it fully controls, corrupting whatever the firmware placed after it in the global object.

  • Weakness: CWE-787 (out-of-bounds write) plus CWE-131 (incorrect calculation of buffer size)
  • Severity: medium (no CVSS score assigned)
  • Affected: ArduinoBLE <= 2.0.1. Fixed: 2.0.2
  • Precondition: the device exposes at least one writable characteristic with the BLEEncryption permission

Background: the ATT write path

ArduinoBLE is the Bluetooth Low Energy stack for Arduino boards (Nano 33 BLE, Nano RP2040 Connect, and others). At its core is a single global ATTClass object, ATT, that parses incoming ATT protocol PDUs. Writes to a GATT characteristic arrive as ATT_OP_WRITE_REQ or ATT_OP_WRITE_CMD and land in ATTClass::writeReqOrCmd().

A characteristic can be marked as requiring encryption with the BLEEncryption permission. If a client tries to write to such a characteristic before the connection is AES-encrypted, ArduinoBLE does not simply drop the request. It replies with INSUFF_ENC and stashes the pending write in a member buffer, so it can be replayed once the link becomes encrypted.

That stash is where the bug lives. It runs on the pre-encryption path, which is exactly the path an unauthenticated attacker reaches first.

The bug: a memcpy with no bounds check

The stash buffer is a fixed 64 bytes, a plain member of the global object:

src/utility/ATT.h
uint8_t writeBuffer[64];
uint8_t writeBufferSize;

Inside writeReqOrCmd(), the length and pointer of the value come straight from the incoming PDU, and the “hold for encryption” branch fills writeBuffer by hand:

src/utility/ATT.cpp, ATTClass::writeReqOrCmd() (vulnerable, <= 2.0.1)
uint8_t valueLength = dlen - sizeof(handle);   // attacker-controlled ATT PDU length
uint8_t* value      = &data[sizeof(handle)];
// ...
if (holdResponse) {                            // set when BLEEncryption is required
    writeBufferSize = 0;                       //   and the peer is not yet encrypted
    memcpy(writeBuffer, &handle, 2);              writeBufferSize += 2;
    writeBuffer[writeBufferSize++] = _peers[i].addressType;
    memcpy(&writeBuffer[writeBufferSize], _peers[i].address, 6);
    writeBufferSize += 6;
    writeBuffer[writeBufferSize] = valueLength;   writeBufferSize += 1;

    // no check that writeBufferSize + valueLength fits in writeBuffer
    memcpy(&writeBuffer[writeBufferSize], value, valueLength);   // out-of-bounds write
    writeBufferSize += valueLength;
}

Count the header it writes before the value: handle (2) plus addressType (1) plus address (6) plus valueLength (1) equals 10 bytes. That leaves 54 bytes of headroom in the 64-byte buffer.

But valueLength is a uint8_t derived from the length of the incoming ATT PDU, and nothing in the handler bounds it against writeBuffer. That length comes up from the L2CAP layer: ArduinoBLE reassembles the incoming PDU into a 255-byte L2CAP buffer, so an attacker can push valueLength up to roughly 248 bytes. The handler never checks the write length against the buffer, and it does not even check it against the negotiated MTU. A single oversized write therefore runs the memcpy well off the end of the 64-byte writeBuffer and into whatever the firmware placed after it in the global ATTClass object. No MTU negotiation is needed: it fires at the default 23-byte MTU, which ArduinoBLE caps and never enforces on the write length anyway.

The value bytes are entirely attacker-controlled. This is not a random smash, it is an overflow with chosen content, delivered by an unauthenticated radio.

Who can trigger it

Everything the attacker needs is on the pre-encryption path, so no pairing, bonding, or key is involved:

  • The target device exposes at least one writable characteristic carrying the BLEEncryption permission.
  • The attacker connects over BLE, no pairing or bonding, and sends a write request to that characteristic’s handle carrying a value longer than the 54 bytes of headroom, fragmented over L2CAP if needed.
  • No MTU exchange is required. The handler never checks the write length against the MTU, and ArduinoBLE reassembles the oversized PDU into its 255-byte L2CAP buffer on its own.

The INSUFF_ENC hold path fires, the oversized value is copied past the buffer, and the global object is corrupted. No user interaction, no authentication.

What this enables

An honesty note first: we did not build a proof-of-concept for this bug. What follows is reasoned analysis of the primitive, grounded in the source above, not demonstrated exploitation. The primitive itself is clear enough to reason about: a remote, unauthenticated client, with no user interaction, gets a controlled-content out-of-bounds write that starts at a fixed offset (10 bytes) inside the 64-byte writeBuffer and runs on into the rest of the global ATTClass object.

Not demonstrated. We have not run this on hardware. The tiers below separate what the primitive reliably gives from what would need a per-target exploit we did not write.

Tier 1, remote denial of service (expected). Overflowing a live global object corrupts the BLE stack’s own state while it is running. The reliable, portable outcome is a crash or a wedged radio: a single oversized write knocks the peripheral offline until reboot. This needs nothing beyond the overflow itself and does not depend on the exact memory layout.

Tier 2, targeted state corruption (conditional). Because the overflow content is attacker-chosen, the bytes right after writeBuffer are a lever, not just collateral. The immediate neighbours are holdBufferSize then writeBufferSize; past them sit the rest of the ATTClass object (the _peers connection table, ATT bookkeeping, pointers) and whatever the linker placed after the global. Overwriting these could desynchronize the ATT state machine, corrupt stored peer entries, or tamper with the very pending write that gets replayed once the link is encrypted. All of this is conditional on the object’s layout in a given build.

Tier 3, control-flow hijack (speculative, per-target). If a function pointer, or a structure the stack later dereferences, sits within reach of the overflow on a specific firmware image, a chosen-content write could in principle redirect execution at the privilege of the BLE task. Whether such a target is actually in range depends entirely on the board, the ArduinoBLE build, and the linker layout. We have not identified one and make no general claim. This is the ceiling to investigate per device, not a result.

The portable, defensible impact is a remote, pre-authentication denial of service. Anything beyond that is target-specific and would need a per-firmware exploit. Arduino rated the issue medium, and no CVSS score was assigned.

The fix

Version 2.0.2 adds the one check that was missing, right before the copy:

the fix (ArduinoBLE 2.0.2)
if (writeBufferSize + valueLength > sizeof(writeBuffer)) {
    sendError(connectionHandle, op, handle, ATT_ECODE_INSUFF_RESOURCES);
    return;
}
memcpy(&writeBuffer[writeBufferSize], value, valueLength);

If the pending write does not fit, it is rejected with INSUFF_RESOURCES instead of copied. Four lines. If you ship ArduinoBLE, update to 2.0.2.

Affected Versions

VersionStatus
<= 2.0.1Vulnerable
2.0.2 and laterFixed

Fix commit 1460e1a (PR #431), released in ArduinoBLE 2.0.2.

Timeline

Coordinated disclosure with Arduino. The advisory and the fixed release (2.0.2) were published on 2026-06-03.

References

  • GHSA-77v6-cw9f-9whg, Arduino advisory for CVE-2026-47773
  • ArduinoBLE PR #431, the fix
  • src/utility/ATT.cpp, ATTClass::writeReqOrCmd(), the vulnerable handler
  • src/utility/ATT.h, the 64-byte writeBuffer member