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:

  1. Single large write: Send a GATT write whose decoded payload exceeds CONFIG_POUCH_SERVER_CERT_MAX_LEN in one shot.
  2. 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.
  • memcpy writes past value[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:

  1. The > check passes (it should be >=).
  2. strncpy copies exactly CONFIG_GOLIOTH_COAP_MAX_PATH_LEN bytes and does not append a NUL terminator (this is strncpy’s documented behavior when the source length equals or exceeds the count).
  3. Later, strlen(ctx->path) in coap_client.c reads 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:

1
BLE Discovery (unauthenticated)
Attacker scans for BLE GATT services, discovers server cert characteristic. No pairing or bonding required.
2
Certificate Injection (Vuln 1)
Write attacker-controlled cert via GATT (BT_GATT_PERM_WRITE, no auth). Device stores it via pouch_server_certificate_set().
3
MITM Established
Device authenticates server using injected cert. Attacker holds the private key, intercepts and proxies the connection.
4
Exploitation (Vuln 2 or Vuln 3)
Memory Disclosure
Vuln 2
Send 1-byte string response → size_t underflow → OOB read → leak adjacent heap memory (keys, creds)
Remote Code Execution
Vuln 3
Send oversized integer response → assert() absent in release → memcpy overflows 12-byte stack buffer → overwrite return address → RCE

Step-by-Step Walkthrough

  1. BLE Discovery (~10-100m range): The attacker uses a BLE scanner (e.g., nRF Connect, a custom bleak script) to discover the Golioth GATT service and the server certificate characteristic UUID.

  2. Certificate Injection: The attacker writes a valid, attacker-controlled X.509 certificate to the characteristic. Because BT_GATT_PERM_WRITE requires no authentication, this succeeds on first connection. The device calls pouch_server_certificate_set() and persists the certificate. No overflow is needed for this step. The certificate just needs to be within CONFIG_POUCH_SERVER_CERT_MAX_LEN.

  3. 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_CERT to be disabled; with validation enabled (the default), the injected certificate is rejected against the embedded CA and the chain breaks here.

  4. 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 memcpy overwrites 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 NDEBUG is defined. This is the default for production firmware on all supported platforms.
  • Certificate validation: The MITM step requires CONFIG_POUCH_VALIDATE_SERVER_CERT to 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

Oct 31, 2025
SecMate sent initial report to Golioth (security@golioth.io) with findings and proof-of-concept code
Nov 5, 2025
Golioth confirmed receipt and began internal review
Jan 7, 2026
SecMate followed up; noted 90-day disclosure window ending ~Jan 29, 2026
Jan 9, 2026
Golioth confirmed all reported vulnerabilities have been addressed
Jan 15, 2026
Golioth confirmed no CVEs assigned; agreed to end-of-January disclosure; SecMate offered to handle CVE assignment
Jan 16, 2026
CVE IDs assigned by VulnCheck: CVE-2026-23747, CVE-2026-23748, CVE-2026-23749, CVE-2026-23750
Jan 22, 2026
Version ranges provided to VulnCheck; CVE records finalized for Firmware SDK vulnerabilities
Jan 30, 2026
Public disclosure

CVE Summary

CVE-2026-23750BLE GATT Server Cert Heap Overflow
Pouch · Introduced v0.1.0 · Fixed in 1b2219a1
CVE-2026-23748LightDB State String Underflow OOB Read
Firmware SDK · Introduced v0.10.0 · Fixed in d7f55b38 (v0.22.0)
CVE-2026-23747Assert-Guarded Memcpy Stack Overflow
Firmware SDK · Introduced v0.10.0 · Fixed in 57eac06 (v0.22.0)
CVE-2026-23749CoAP Blockwise Unterminated Path OOB Read
Firmware SDK · Introduced v0.19.1 · Fixed in 0e788217 (v0.22.0)

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