Four Memory Safety Vulnerabilities in Golioth’s IoT Firmware
We discovered four memory safety vulnerabilities in Golioth’s Firmware SDK and Pouch BLE protocol: an unauthenticated BLE heap overflow, an integer underflow causing out-of-bounds reads, a stack buffer overflow guarded only by assert(), and an off-by-one strncpy bug. Each is independently triggerable under its own preconditions.
Three of the four chain together into a path from unauthenticated BLE proximity to remote code execution. Golioth’s defense-in-depth blocks this chain: server certificate validation (CONFIG_POUCH_VALIDATE_SERVER_CERT) is enabled by default and rejects injected certificates at step two.
What is Golioth?
Golioth [1] is an IoT device infrastructure platform for fleet management, OTA updates, data routing, and device monitoring. It ships two open-source products relevant to this research:
The Golioth Firmware SDK [1] is a C library for device-to-cloud communication over CoAP/DTLS [3]. It supports LightDB State (device-shadow key/value store), OTA firmware updates, logging, and settings. The SDK targets Zephyr RTOS, nRF Connect SDK, ESP-IDF, and Infineon ModusToolbox, running on hardware including the nRF9160, nRF52840, and ESP32.
Golioth Pouch [2] is a non-IP protocol for device-to-cloud communication through BLE gateways. Pouch uses BLE GATT for the device-to-gateway leg, with end-to-end encryption to the Golioth cloud. It is designed around an untrusted gateway model: gateways relay data but cannot inspect it. A BLE GATT server on the device exchanges certificates with a gateway, which then brokers the encrypted session.
SecMate’s automated analysis identified four memory safety vulnerabilities spanning both products.
One finding is particularly worth highlighting: a stack buffer overflow whose only bounds check is assert(), which the C standard defines as a debugging aid and which is compiled out of every release build. This pattern, using assert() as a security boundary, is common across embedded codebases and is almost always a vulnerability in production.
Architecture Overview
Firmware SDK: Device to Cloud
The Firmware SDK handles all cloud communication over CoAP/DTLS. The device authenticates to Golioth using a pre-provisioned certificate or PSK, then exchanges CoAP messages for state synchronization (LightDB State), firmware updates, and telemetry. Payloads arrive as raw byte buffers with an associated payload_size, and the parsing code trusts that relationship implicitly. The vulnerable code paths are in lightdb_state.c, payload_utils.c, coap_blockwise.c, and coap_client.c.
Pouch: Non-IP Gateway Protocol
Pouch is a non-IP protocol designed to route device traffic through BLE gateways to the Golioth cloud. Its security model is built around an untrusted gateway assumption: gateways are treated as opaque relays that forward data but cannot inspect or modify it, thanks to end-to-end encryption between the device and cloud. This is a deliberate design choice: the protocol explicitly considers the scenario where an intermediary gateway is compromised.
What makes the first vulnerability particularly notable is that Pouch’s threat model accounts for gateway compromise but not for unauthenticated BLE-level write access. The server certificate characteristic is registered with BT_GATT_PERM_WRITE, meaning any BLE client in range can write to it without pairing, bonding, or encryption. The untrusted gateway model protects data in transit, but the certificate exchange itself has no access control. This is the entry point for the attack chain.
Vulnerability #1: BLE GATT Server Certificate Heap Overflow (CVE-2026-23750 [4])
Location: server_cert_write() in server_cert_characteristic.c (Golioth Pouch)
This is the lead vulnerability and the entry point for the full chain. Any BLE client within radio range can trigger it: no pairing, no bonding, no authentication of any kind.
The Bug
The server_cert_write handler processes incoming BLE GATT writes to the server certificate characteristic. When the first fragment arrives, it allocates a fixed-size heap buffer. For every fragment, it blindly appends the payload:
1// server_cert_characteristic.c:105-153
2static ssize_t server_cert_write(struct bt_conn *conn,
3 const struct bt_gatt_attr *attr,
4 const void *buf,
5 uint16_t len,
6 uint16_t offset,
7 uint8_t flags)
8{
9 struct golioth_ble_gatt_server_cert_ctx *ctx = attr->user_data;
10 bool is_first = false;
11 bool is_last = false;
12 const void *payload = NULL;
13 ssize_t payload_len =
14 golioth_ble_gatt_packetizer_decode(buf, len, &payload, &is_first, &is_last);
15
16 if (0 >= payload_len)
17 {
18 return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
19 }
20
21 if (is_first)
22 {
23 free((void *) ctx->cert.buffer);
24 ctx->cert.buffer = malloc(CONFIG_POUCH_SERVER_CERT_MAX_LEN); // Fixed-size allocation
25 ctx->cert.size = 0;
26 }
27
28 if (!ctx->cert.buffer)
29 {
30 return BT_GATT_ERR(BT_ATT_ERR_INSUFFICIENT_RESOURCES);
31 }
32
33 // ** VULNERABILITY: No check that ctx->cert.size + payload_len
34 // <= CONFIG_POUCH_SERVER_CERT_MAX_LEN **
35 memcpy((void *) &ctx->cert.buffer[ctx->cert.size], payload, payload_len);
36 ctx->cert.size += payload_len;
37 // ...
38}
The critical issue is the memcpy call: it copies payload_len bytes into ctx->cert.buffer at offset ctx->cert.size without ever checking whether the cumulative size exceeds CONFIG_POUCH_SERVER_CERT_MAX_LEN. The packetizer decode function returns a length derived directly from the GATT write size. No upper bound is enforced.
The Permission Model
The characteristic is registered with:
1// server_cert_characteristic.c:155-161
2GOLIOTH_BLE_GATT_CHARACTERISTIC(server_cert,
3 (const struct bt_uuid *) &golioth_ble_gatt_server_cert_chrc_uuid,
4 BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
5 BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, // No auth/encryption
6 server_cert_serial_read,
7 server_cert_write,
8 &server_cert_chrc_ctx);
BT_GATT_PERM_WRITE grants write access to any connected BLE client. Compare this with BT_GATT_PERM_WRITE_ENCRYPT or BT_GATT_PERM_WRITE_AUTHEN, which require an encrypted link or authenticated pairing respectively. The weakest permission level was chosen.
Impact
An attacker can overflow the heap buffer in two ways:
- Single large write: Send a GATT write whose decoded payload exceeds
CONFIG_POUCH_SERVER_CERT_MAX_LENin one shot. - Fragmented overflow: Send multiple fragments whose cumulative size exceeds the allocation.
On MCUs with deterministic heap layouts (no ASLR), the immediate impact is a crash (denial of service). Depending on the allocator implementation and heap layout, heap metadata corruption may also enable controlled overwrites, but exploitation reliability varies by target and configuration.
Beyond heap overflow, there is a subtler but equally dangerous impact: certificate injection. If the attacker writes a valid but attacker-controlled certificate within the buffer bounds (no overflow needed, just a normal-sized write), the device will accept it via pouch_server_certificate_set() and use it for subsequent encrypted sessions. This establishes a man-in-the-middle position without triggering any crash. Certificate injection opens the door to everything that follows.
Note: Pouch provides a server certificate validation option (CONFIG_POUCH_VALIDATE_SERVER_CERT, enabled by default) that checks injected certificates against an embedded CA. When this validation is active, the certificate injection is rejected. The attack chain requires this validation to be disabled, a configuration the build system explicitly warns against for production use.
Vulnerability #2: LightDB State String Underflow Out-of-Bounds Read (CVE-2026-23748 [5])
Location: lightdb_state.c (Golioth Firmware SDK)
Chain Context
With a rogue certificate injected via Vulnerability #1 and server certificate validation disabled, the attacker can sit in the middle of the device encrypted session to the Golioth cloud. From this position, they can craft arbitrary CoAP responses to any LightDB State query the device makes.
The Bug
When the SDK receives a LightDB State response for a string value, it strips the surrounding JSON quotes by copying from payload + 1 for payload_size - 2 bytes:
1// lightdb_state.c:408-414
2case LIGHTDB_GET_TYPE_STRING:
3{
4 // Remove the leading and trailing quote to get the raw string value
5 size_t nbytes = min(ldb_response->buf_size - 1, payload_size - 2); // <-- UNDERFLOW
6 memcpy(ldb_response->buf, payload + 1 /* skip quote */, nbytes);
7 ldb_response->buf[nbytes] = 0;
8}
9break;
The Math
payload_size is a size_t, an unsigned type. When the server responds with a 1-byte payload:
payload_size = 1
payload_size - 2 = 1 - 2
= (size_t)(-1)
= 0xFFFFFFFF (on 32-bit MCU)
= 4,294,967,295
The min() macro then selects ldb_response->buf_size - 1 (the smaller value, e.g., 63 or 127 depending on the caller’s buffer). The memcpy then reads that many bytes starting from payload + 1, which is already one byte past the end of the actual 1-byte payload buffer.
Why This Passes the Null Check
The code does check for null payloads before reaching this path:
1// lightdb_state.c:387-391
2if (golioth_payload_is_null(payload, payload_size))
3{
4 ldb_response->is_null = true;
5 return;
6}
But golioth_payload_is_null() only returns true for NULL pointers, zero-length payloads, or payloads starting with the string "null". A 1-byte payload like '"' passes through.
Impact
The memcpy reads buf_size - 1 bytes from memory adjacent to the payload buffer. On embedded devices with flat memory models, this can leak:
- DTLS session keys stored nearby in heap memory
- WiFi/BLE credentials
- Other application secrets
Even without useful data in adjacent memory, the out-of-bounds read will likely fault on unmapped regions, causing a device crash (DoS).
Vulnerability #3: Assert-Guarded Memcpy Stack Buffer Overflow (CVE-2026-23747 [6])
Location: golioth_payload_as_int() / golioth_payload_as_float() in payload_utils.c (Golioth Firmware SDK)
Chain Context
Still operating as MITM via the injected certificate, the attacker crafts an oversized integer or float payload in response to a LightDB State query.
The Bug
The payload parsing helpers use assert() as their sole bounds check:
1// payload_utils.c:15-23
2int32_t golioth_payload_as_int(const uint8_t *payload, size_t payload_size)
3{
4 // Copy payload to a NULL-terminated string
5 char value[12] = {};
6 assert(payload_size <= sizeof(value)); // <-- Only defense
7 memcpy(value, payload, payload_size); // <-- Unbounded in release
8
9 return strtol(value, NULL, 10);
10}
11
12// payload_utils.c:25-33
13float golioth_payload_as_float(const uint8_t *payload, size_t payload_size)
14{
15 // Copy payload to a NULL-terminated string
16 char value[32] = {};
17 assert(payload_size <= sizeof(value)); // <-- Only defense
18 memcpy(value, payload, payload_size); // <-- Unbounded in release
19
20 return strtof(value, NULL);
21}
These functions are called directly from the network-facing on_payload() callback:
1// lightdb_state.c:395-396
2case LIGHTDB_GET_TYPE_INT:
3 *ldb_response->i = golioth_payload_as_int(payload, payload_size);
4 break;
The Anti-Pattern: assert() as a Security Check
The C standard defines assert() as a debugging aid. When the macro NDEBUG is defined, which is standard practice in release/production builds, assert() expands to nothing:
// <assert.h> with NDEBUG defined:
#define assert(expression) ((void)0)
Here is what the compiler sees in each build configuration:
Debug build (assert active):
1char value[12] = {};
2// assert fires, aborts if payload_size > 12
3((payload_size <= sizeof(value)) ? (void)0 : __assert_fail(...));
4memcpy(value, payload, payload_size);
Release build (NDEBUG defined):
1char value[12] = {};
2// assert compiled away --- nothing here
3((void)0);
4memcpy(value, payload, payload_size); // copies whatever the network sent
In the release build, memcpy copies payload_size bytes, directly controlled by the network, into a 12-byte (or 32-byte) stack buffer with no bounds check whatsoever.
Impact
On MCUs where stack canaries are not enabled (common in Zephyr and ESP-IDF default configurations for size and performance reasons), this is a textbook stack buffer overflow:
- The attacker sends a LightDB State integer response with 100+ bytes of payload.
memcpywrites pastvalue[12], overwriting the saved frame pointer, return address, and any other locals on the stack.- On Cortex-M (ARM Thumb), the saved return address on the stack is loaded into the PC. The attacker controls the PC.
- With a deterministic memory layout and a known firmware image, this enables control flow hijack and potential RCE on embedded targets without common mitigations.
The float variant (value[32]) requires a slightly larger payload but is otherwise identical.
Vulnerability #4: CoAP Blockwise Unterminated Path Out-of-Bounds Read (CVE-2026-23749 [7])
Location: blockwise_transfer_init() in coap_blockwise.c (Golioth Firmware SDK)
This vulnerability is locally triggered (by the application’s own path strings) rather than network-exploitable. We include it to complete the pattern of C memory safety hazards found across the SDK.
The Bug
1// coap_blockwise.c:76-94
2static int blockwise_transfer_init(struct blockwise_transfer *ctx,
3 struct golioth_client *client,
4 const char *path_prefix,
5 const char *path,
6 enum golioth_content_type content_type)
7{
8 if (strlen(path) > CONFIG_GOLIOTH_COAP_MAX_PATH_LEN) // allows ==
9 {
10 return -EINVAL;
11 }
12 strncpy(ctx->path, path, CONFIG_GOLIOTH_COAP_MAX_PATH_LEN); // no NUL if len == max
13 // ...
14}
The buffer is declared as char path[CONFIG_GOLIOTH_COAP_MAX_PATH_LEN + 1], providing space for a NUL terminator. However, when strlen(path) is exactly CONFIG_GOLIOTH_COAP_MAX_PATH_LEN:
- The
>check passes (it should be>=). strncpycopies exactlyCONFIG_GOLIOTH_COAP_MAX_PATH_LENbytes and does not append a NUL terminator (this isstrncpy’s documented behavior when the source length equals or exceeds the count).- Later,
strlen(ctx->path)incoap_client.creads past the buffer searching for a NUL byte.
This is a classic strncpy misunderstanding. Many C developers assume strncpy always NUL-terminates. It does not when the source string fills the destination.
Impact
The out-of-bounds strlen() read can cause:
- A crash if it walks into unmapped memory
- Incorrect path construction if it finds a stale NUL byte further in memory
- On safety-critical IoT devices (medical, industrial), even a locally-triggered crash can have real-world consequences
Theoretical Chain: BLE Proximity to Device Compromise
The following attack chain is theoretical. It has not been demonstrated end to end. It requires CONFIG_POUCH_VALIDATE_SERVER_CERT to be disabled, which is not default and the build system explicitly warns against for production use. With certificate validation enabled, the default, the chain breaks at step 2 and injected certificates are rejected.
That said, the chain illustrates how individual memory safety bugs compound when defense-in-depth is weakened. If certificate validation were disabled, three of the four vulnerabilities could connect into a path from unauthenticated BLE proximity to arbitrary code execution:
BT_GATT_PERM_WRITE, no auth). Device stores it via pouch_server_certificate_set().size_t underflow → OOB read → leak adjacent heap memory (keys, creds)assert() absent in release → memcpy overflows 12-byte stack buffer → overwrite return address → RCEStep-by-Step Walkthrough
BLE Discovery (~10-100m range): The attacker uses a BLE scanner (e.g.,
nRF Connect, a custombleakscript) to discover the Golioth GATT service and the server certificate characteristic UUID.Certificate Injection: The attacker writes a valid, attacker-controlled X.509 certificate to the characteristic. Because
BT_GATT_PERM_WRITErequires no authentication, this succeeds on first connection. The device callspouch_server_certificate_set()and persists the certificate. No overflow is needed for this step. The certificate just needs to be withinCONFIG_POUCH_SERVER_CERT_MAX_LEN.MITM Established: When the device next initiates a session to the Golioth cloud, it uses the injected certificate to authenticate the server. The attacker, holding the corresponding private key, terminates the connection from the device and opens a separate connection to the real Golioth cloud, proxying traffic between the two. This step requires
CONFIG_POUCH_VALIDATE_SERVER_CERTto be disabled; with validation enabled (the default), the injected certificate is rejected against the embedded CA and the chain breaks here.Exploitation: From the MITM position, the attacker waits for, or triggers, a LightDB State query from the device, then:
- For memory disclosure (Vuln 2): Responds with a 1-byte payload to a string GET, causing the underflow and OOB read. Leaked memory may contain DTLS keys or other secrets.
- For code execution (Vuln 3): Responds with an oversized payload to an integer GET. In release builds, the assert is gone, and
memcpyoverwrites the stack. On Cortex-M with no ASLR or stack canaries, the return address is overwritten with an attacker-controlled value.
Feasibility and Limitations
- BLE range constraint: The initial step requires physical proximity. In practice, directional antennas can extend BLE range significantly, and many IoT deployments are in physically accessible locations.
- Certificate persistence: Whether the injected certificate persists across reboots depends on the Pouch configuration and storage backend. If persistent, the MITM survives power cycles.
- Release build requirement: Vulnerability 3 requires that
NDEBUGis defined. This is the default for production firmware on all supported platforms. - Certificate validation: The MITM step requires
CONFIG_POUCH_VALIDATE_SERVER_CERTto be disabled. This option is enabled by default and validates server certificates against an embedded CA. When enabled, certificate injection is blocked. - Vulnerability 4 is independent: The CoAP blockwise path issue is locally triggered and is not part of this chain.
Conclusion
Four memory safety vulnerabilities across two Golioth IoT products. Each is independently triggerable under its own preconditions. The BLE heap overflow, Vuln 1, is the most immediately dangerous: any device running Pouch with BLE enabled can be crashed or have its certificate store corrupted by an unauthenticated attacker in radio range.
The theoretical chain from BLE to RCE requires certificate validation to be disabled, which is not the default. But the individual bugs do not require special configuration. A 1-byte CoAP response triggers the integer underflow. An oversized integer payload overwrites the stack in any release build. These are real risks independent of any chain.
The bugs themselves are not exotic. They are memcpy without bounds checks, assert instead of if, strncpy without NUL termination, unsigned subtraction without underflow guards. These are patterns that every C programmer learns to avoid, and that every C codebase eventually contains unless the review process catches them.
Embedded firmware needs the same level of scrutiny as kernel code. It runs with equivalent privilege (there is no lower ring on a Cortex-M), handles untrusted input from multiple interfaces (BLE, WiFi, CoAP, MQTT), and deploys to devices that may be physically accessible to attackers and difficult to update after deployment.
Disclosure and Fix
We reported all four vulnerabilities to Golioth through coordinated disclosure. The team responded promptly and confirmed all fixes before the disclosure deadline. SecMate would like to thank the Golioth team and maintainers for their responsiveness and security awareness. Fixes are available in Firmware SDK v0.22.0 and Pouch commit 1b2219a1.
Disclosure Timeline
security@golioth.io) with findings and proof-of-concept codeCVE Summary
What’s Next
This is our second public disclosure, after the libcoap vulnerabilities we published earlier. These four CVEs bring our public total to six, with more findings across embedded and systems projects still in coordinated disclosure.
For the full list, see our disclosure page. If you’re building on embedded systems and want to find vulnerabilities before attackers do, reach out.
References
[1] Golioth. “Golioth Firmware SDK” GitHub. Repository
[2] Golioth. “Golioth Pouch” GitHub. Repository
[3] IETF. “RFC 7252 - The Constrained Application Protocol (CoAP)” IETF Datatracker, June 2014. RFC
[4] NVD. “CVE-2026-23750 - BLE GATT Server Certificate Heap Overflow in Golioth Pouch” National Vulnerability Database. CVE
[5] NVD. “CVE-2026-23748 - LightDB State String Underflow OOB Read in Golioth Firmware SDK” National Vulnerability Database. CVE
[6] NVD. “CVE-2026-23747 - Assert-Guarded Memcpy Stack Overflow in Golioth Firmware SDK” National Vulnerability Database. CVE
[7] NVD. “CVE-2026-23749 - CoAP Blockwise Unterminated Path OOB Read in Golioth Firmware SDK” National Vulnerability Database. CVE
The SecMate Team