<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>golioth on SecMate Blog</title><link>https://blog.secmate.dev/tags/golioth/</link><description>What we find. What we share.</description><generator>Hugo</generator><language>en-us</language><managingEditor>noreply@blog.secmate.dev//blog.secmate.dev/ (SecMate Team)</managingEditor><lastBuildDate>Tue, 25 Aug 2026 10:39:55 +0000</lastBuildDate><atom:link href="https://blog.secmate.dev/tags/golioth/index.xml" rel="self" type="application/rss+xml"/><item><title>Four Memory Safety Vulnerabilities in Golioth's IoT Firmware</title><link>https://blog.secmate.dev/posts/golioth-vulnerabilities-disclosure/</link><pubDate>Wed, 25 Feb 2026 00:00:00 +0000</pubDate><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>IoT</category><guid>https://blog.secmate.dev/posts/golioth-vulnerabilities-disclosure/</guid><description>SecMate details CVE-2026-23747 through CVE-2026-23750 in Golioth's Firmware SDK and Pouch BLE protocol: four independently triggerable memory-safety flaws.</description><content:encoded><![CDATA[<p>We discovered four memory safety vulnerabilities in Golioth&rsquo;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 <code>assert()</code>, and an off-by-one <code>strncpy</code> bug. Each is independently triggerable under its own preconditions.</p>
<p>Three of the four chain together into a path from unauthenticated BLE proximity to remote code execution. Golioth&rsquo;s defense-in-depth blocks this chain: server certificate validation (<code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code>) is enabled by default and rejects injected certificates at step two.</p>
<h2 id="what-is-golioth">What is Golioth?</h2>
<p><a href="https://golioth.io/" rel="noopener noreferrer" target="_blank">Golioth</a> <a href="#ref1">[1]</a> 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:</p>
<p>The <strong>Golioth Firmware SDK</strong> <a href="#ref1">[1]</a> is a C library for device-to-cloud communication over CoAP/DTLS <a href="#ref3">[3]</a>. 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.</p>
<p><strong>Golioth Pouch</strong> <a href="#ref2">[2]</a> 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 <strong>untrusted gateway model</strong>: 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.</p>
<p>SecMate&rsquo;s automated analysis identified <strong>four memory safety vulnerabilities</strong> spanning both products.</p>
<p>One finding is particularly worth highlighting: a stack buffer overflow whose only bounds check is <code>assert()</code>, which the C standard defines as a debugging aid and which is compiled out of every release build. This pattern, using <code>assert()</code> as a security boundary, is common across embedded codebases and is almost always a vulnerability in production.</p>
<h2 id="architecture-overview">Architecture Overview</h2>
<h3 id="firmware-sdk-device-to-cloud">Firmware SDK: Device to Cloud</h3>
<p>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 <code>payload_size</code>, and the parsing code trusts that relationship implicitly. The vulnerable code paths are in <code>lightdb_state.c</code>, <code>payload_utils.c</code>, <code>coap_blockwise.c</code>, and <code>coap_client.c</code>.</p>
<h3 id="pouch-non-ip-gateway-protocol">Pouch: Non-IP Gateway Protocol</h3>
<p>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 <strong>untrusted gateway assumption</strong>: 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.</p>
<p>What makes the first vulnerability particularly notable is that Pouch&rsquo;s threat model accounts for gateway compromise but <em>not</em> for unauthenticated BLE-level write access. The server certificate characteristic is registered with <code>BT_GATT_PERM_WRITE</code>, meaning <strong>any BLE client in range can write to it without pairing, bonding, or encryption</strong>. 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.</p>
<h2 id="vulnerability-1-ble-gatt-server-certificate-heap-overflow-cve-2026-23750-4ref4">Vulnerability #1: BLE GATT Server Certificate Heap Overflow (CVE-2026-23750 <a href="#ref4">[4]</a>)</h2>
<p><strong>Location</strong>: <code>server_cert_write()</code> in <code>server_cert_characteristic.c</code> (Golioth Pouch)</p>
<p>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.</p>
<h3 id="the-bug">The Bug</h3>
<p>The <code>server_cert_write</code> 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:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// server_cert_characteristic.c:105-153
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">server_cert_write</span><span class="p">(</span><span class="k">struct</span> <span class="n">bt_conn</span> <span class="o">*</span><span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                                 <span class="k">const</span> <span class="k">struct</span> <span class="n">bt_gatt_attr</span> <span class="o">*</span><span class="n">attr</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                 <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                 <span class="kt">uint16_t</span> <span class="n">len</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                                 <span class="kt">uint16_t</span> <span class="n">offset</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                                 <span class="kt">uint8_t</span> <span class="n">flags</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">struct</span> <span class="n">golioth_ble_gatt_server_cert_ctx</span> <span class="o">*</span><span class="n">ctx</span> <span class="o">=</span> <span class="n">attr</span><span class="o">-&gt;</span><span class="n">user_data</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kt">bool</span> <span class="n">is_first</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kt">bool</span> <span class="n">is_last</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">payload</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="kt">ssize_t</span> <span class="n">payload_len</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nf">golioth_ble_gatt_packetizer_decode</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">payload</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">is_first</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">is_last</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&gt;=</span> <span class="n">payload_len</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">return</span> <span class="nf">BT_GATT_ERR</span><span class="p">(</span><span class="n">BT_ATT_ERR_UNLIKELY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">is_first</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="nf">free</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="nf">malloc</span><span class="p">(</span><span class="n">CONFIG_POUCH_SERVER_CERT_MAX_LEN</span><span class="p">);</span>  <span class="c1">// Fixed-size allocation
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span>        <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="k">return</span> <span class="nf">BT_GATT_ERR</span><span class="p">(</span><span class="n">BT_ATT_ERR_INSUFFICIENT_RESOURCES</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="c1">// ** VULNERABILITY: No check that ctx-&gt;cert.size + payload_len
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span>    <span class="c1">//    &lt;= CONFIG_POUCH_SERVER_CERT_MAX_LEN **
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">[</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span><span class="p">],</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_len</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span> <span class="o">+=</span> <span class="n">payload_len</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>The critical issue is the <code>memcpy</code> call: it copies <code>payload_len</code> bytes into <code>ctx-&gt;cert.buffer</code> at offset <code>ctx-&gt;cert.size</code> without ever checking whether the cumulative size exceeds <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code>. The packetizer decode function returns a length derived directly from the GATT write size. No upper bound is enforced.</p>
<h3 id="the-permission-model">The Permission Model</h3>
<p>The characteristic is registered with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// server_cert_characteristic.c:155-161
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nf">GOLIOTH_BLE_GATT_CHARACTERISTIC</span><span class="p">(</span><span class="n">server_cert</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                <span class="p">(</span><span class="k">const</span> <span class="k">struct</span> <span class="n">bt_uuid</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">golioth_ble_gatt_server_cert_chrc_uuid</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                <span class="n">BT_GATT_CHRC_READ</span> <span class="o">|</span> <span class="n">BT_GATT_CHRC_WRITE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                <span class="n">BT_GATT_PERM_READ</span> <span class="o">|</span> <span class="n">BT_GATT_PERM_WRITE</span><span class="p">,</span>  <span class="c1">// No auth/encryption
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>                                <span class="n">server_cert_serial_read</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">                                <span class="n">server_cert_write</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">                                <span class="o">&amp;</span><span class="n">server_cert_chrc_ctx</span><span class="p">);</span>
</span></span></code></pre></div><p><code>BT_GATT_PERM_WRITE</code> grants write access to any connected BLE client. Compare this with <code>BT_GATT_PERM_WRITE_ENCRYPT</code> or <code>BT_GATT_PERM_WRITE_AUTHEN</code>, which require an encrypted link or authenticated pairing respectively. The weakest permission level was chosen.</p>
<h3 id="impact">Impact</h3>
<p>An attacker can overflow the heap buffer in two ways:</p>
<ol>
<li><strong>Single large write</strong>: Send a GATT write whose decoded payload exceeds <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code> in one shot.</li>
<li><strong>Fragmented overflow</strong>: Send multiple fragments whose cumulative size exceeds the allocation.</li>
</ol>
<p>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.</p>
<p>Beyond heap overflow, there is a subtler but equally dangerous impact: <strong>certificate injection</strong>. If the attacker writes a valid but attacker-controlled certificate <em>within</em> the buffer bounds (no overflow needed, just a normal-sized write), the device will accept it via <code>pouch_server_certificate_set()</code> 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.</p>
<p>Note: Pouch provides a server certificate validation option (<code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code>, 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.</p>
<h2 id="vulnerability-2-lightdb-state-string-underflow-out-of-bounds-read-cve-2026-23748-5ref5">Vulnerability #2: LightDB State String Underflow Out-of-Bounds Read (CVE-2026-23748 <a href="#ref5">[5]</a>)</h2>
<p><strong>Location</strong>: <code>lightdb_state.c</code> (Golioth Firmware SDK)</p>
<h3 id="chain-context">Chain Context</h3>
<p>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.</p>
<h3 id="the-bug-1">The Bug</h3>
<p>When the SDK receives a LightDB State response for a string value, it strips the surrounding JSON quotes by copying from <code>payload + 1</code> for <code>payload_size - 2</code> bytes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// lightdb_state.c:408-414
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">case</span> <span class="nl">LIGHTDB_GET_TYPE_STRING</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// Remove the leading and trailing quote to get the raw string value
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="kt">size_t</span> <span class="n">nbytes</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf_size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">payload_size</span> <span class="o">-</span> <span class="mi">2</span><span class="p">);</span>  <span class="c1">// &lt;-- UNDERFLOW
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">,</span> <span class="n">payload</span> <span class="o">+</span> <span class="mi">1</span> <span class="cm">/* skip quote */</span><span class="p">,</span> <span class="n">nbytes</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">[</span><span class="n">nbytes</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="k">break</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="the-math">The Math</h3>
<p><code>payload_size</code> is a <code>size_t</code>, an unsigned type. When the server responds with a 1-byte payload:</p>
<pre tabindex="0"><code>payload_size = 1

payload_size - 2 = 1 - 2
                 = (size_t)(-1)
                 = 0xFFFFFFFF  (on 32-bit MCU)
                 = 4,294,967,295
</code></pre><p>The <code>min()</code> macro then selects <code>ldb_response-&gt;buf_size - 1</code> (the smaller value, e.g., 63 or 127 depending on the caller&rsquo;s buffer). The <code>memcpy</code> then reads that many bytes starting from <code>payload + 1</code>, which is already one byte past the end of the actual 1-byte payload buffer.</p>
<h3 id="why-this-passes-the-null-check">Why This Passes the Null Check</h3>
<p>The code does check for null payloads before reaching this path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// lightdb_state.c:387-391
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nf">golioth_payload_is_null</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">is_null</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>But <code>golioth_payload_is_null()</code> only returns true for <code>NULL</code> pointers, zero-length payloads, or payloads starting with the string <code>&quot;null&quot;</code>. A 1-byte payload like <code>'&quot;'</code> passes through.</p>
<h3 id="impact-1">Impact</h3>
<p>The <code>memcpy</code> reads <code>buf_size - 1</code> bytes from memory adjacent to the payload buffer. On embedded devices with flat memory models, this can leak:</p>
<ul>
<li>DTLS session keys stored nearby in heap memory</li>
<li>WiFi/BLE credentials</li>
<li>Other application secrets</li>
</ul>
<p>Even without useful data in adjacent memory, the out-of-bounds read will likely fault on unmapped regions, causing a device crash (DoS).</p>
<h2 id="vulnerability-3-assert-guarded-memcpy-stack-buffer-overflow-cve-2026-23747-6ref6">Vulnerability #3: Assert-Guarded Memcpy Stack Buffer Overflow (CVE-2026-23747 <a href="#ref6">[6]</a>)</h2>
<p><strong>Location</strong>: <code>golioth_payload_as_int()</code> / <code>golioth_payload_as_float()</code> in <code>payload_utils.c</code> (Golioth Firmware SDK)</p>
<h3 id="chain-context-1">Chain Context</h3>
<p>Still operating as MITM via the injected certificate, the attacker crafts an oversized integer or float payload in response to a LightDB State query.</p>
<h3 id="the-bug-2">The Bug</h3>
<p>The payload parsing helpers use <code>assert()</code> as their sole bounds check:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// payload_utils.c:15-23
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kt">int32_t</span> <span class="nf">golioth_payload_as_int</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">payload</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">payload_size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1">// Copy payload to a NULL-terminated string
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nf">assert</span><span class="p">(</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>   <span class="c1">// &lt;-- Only defense
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>    <span class="c1">// &lt;-- Unbounded in release
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nf">strtol</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// payload_utils.c:25-33
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="kt">float</span> <span class="nf">golioth_payload_as_float</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">payload</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">payload_size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1">// Copy payload to a NULL-terminated string
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>    <span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">32</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nf">assert</span><span class="p">(</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>   <span class="c1">// &lt;-- Only defense
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>    <span class="c1">// &lt;-- Unbounded in release
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="nf">strtof</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>These functions are called directly from the network-facing <code>on_payload()</code> callback:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// lightdb_state.c:395-396
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">case</span> <span class="nl">LIGHTDB_GET_TYPE_INT</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="o">*</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">i</span> <span class="o">=</span> <span class="nf">golioth_payload_as_int</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="the-anti-pattern-assert-as-a-security-check">The Anti-Pattern: <code>assert()</code> as a Security Check</h3>
<p>The C standard defines <code>assert()</code> as a <em>debugging aid</em>. When the macro <code>NDEBUG</code> is defined, which is <strong>standard practice in release/production builds</strong>, <code>assert()</code> expands to nothing:</p>
<pre tabindex="0"><code>// &lt;assert.h&gt; with NDEBUG defined:
#define assert(expression) ((void)0)
</code></pre><p>Here is what the compiler sees in each build configuration:</p>
<p><strong>Debug build</strong> (assert active):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// assert fires, aborts if payload_size &gt; 12
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">((</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="o">?</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="mi">0</span> <span class="o">:</span> <span class="nf">__assert_fail</span><span class="p">(...));</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>Release build</strong> (NDEBUG defined):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// assert compiled away --- nothing here
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">((</span><span class="kt">void</span><span class="p">)</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>  <span class="c1">// copies whatever the network sent
</span></span></span></code></pre></div><p>In the release build, <code>memcpy</code> copies <code>payload_size</code> bytes, directly controlled by the network, into a 12-byte (or 32-byte) stack buffer with no bounds check whatsoever.</p>
<h3 id="impact-2">Impact</h3>
<p>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:</p>
<ul>
<li>The attacker sends a LightDB State integer response with 100+ bytes of payload.</li>
<li><code>memcpy</code> writes past <code>value[12]</code>, overwriting the saved frame pointer, return address, and any other locals on the stack.</li>
<li>On Cortex-M (ARM Thumb), the saved return address on the stack is loaded into the PC. The attacker controls the PC.</li>
<li>With a deterministic memory layout and a known firmware image, this enables control flow hijack and potential RCE on embedded targets without common mitigations.</li>
</ul>
<p>The float variant (<code>value[32]</code>) requires a slightly larger payload but is otherwise identical.</p>
<h2 id="vulnerability-4-coap-blockwise-unterminated-path-out-of-bounds-read-cve-2026-23749-7ref7">Vulnerability #4: CoAP Blockwise Unterminated Path Out-of-Bounds Read (CVE-2026-23749 <a href="#ref7">[7]</a>)</h2>
<p><strong>Location</strong>: <code>blockwise_transfer_init()</code> in <code>coap_blockwise.c</code> (Golioth Firmware SDK)</p>
<p>This vulnerability is locally triggered (by the application&rsquo;s own path strings) rather than network-exploitable. We include it to complete the pattern of C memory safety hazards found across the SDK.</p>
<h3 id="the-bug-3">The Bug</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// coap_blockwise.c:76-94
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="kt">int</span> <span class="nf">blockwise_transfer_init</span><span class="p">(</span><span class="k">struct</span> <span class="n">blockwise_transfer</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                                   <span class="k">struct</span> <span class="n">golioth_client</span> <span class="o">*</span><span class="n">client</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                   <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path_prefix</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                   <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                                   <span class="k">enum</span> <span class="n">golioth_content_type</span> <span class="n">content_type</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">strlen</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</span><span class="p">)</span>  <span class="c1">// allows ==
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nf">strncpy</span><span class="p">(</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">path</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</span><span class="p">);</span>  <span class="c1">// no NUL if len == max
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>The buffer is declared as <code>char path[CONFIG_GOLIOTH_COAP_MAX_PATH_LEN + 1]</code>, providing space for a NUL terminator. However, when <code>strlen(path)</code> is <em>exactly</em> <code>CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</code>:</p>
<ol>
<li>The <code>&gt;</code> check passes (it should be <code>&gt;=</code>).</li>
<li><code>strncpy</code> copies exactly <code>CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</code> bytes and does <strong>not</strong> append a NUL terminator (this is <code>strncpy</code>&rsquo;s documented behavior when the source length equals or exceeds the count).</li>
<li>Later, <code>strlen(ctx-&gt;path)</code> in <code>coap_client.c</code> reads past the buffer searching for a NUL byte.</li>
</ol>
<p>This is a classic <code>strncpy</code> misunderstanding. Many C developers assume <code>strncpy</code> always NUL-terminates. It does not when the source string fills the destination.</p>
<h3 id="impact-3">Impact</h3>
<p>The out-of-bounds <code>strlen()</code> read can cause:</p>
<ul>
<li>A crash if it walks into unmapped memory</li>
<li>Incorrect path construction if it finds a stale NUL byte further in memory</li>
<li>On safety-critical IoT devices (medical, industrial), even a locally-triggered crash can have real-world consequences</li>
</ul>
<h2 id="theoretical-chain-ble-proximity-to-device-compromise">Theoretical Chain: BLE Proximity to Device Compromise</h2>
<p>The following attack chain is <strong>theoretical</strong>. It has not been demonstrated end to end. It requires <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> 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.</p>
<p>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:</p>
<div style="position: relative; padding-left: 2.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: calc(0.25rem + 10px); top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-accent);"></div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-text-secondary); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">1</div>
    <div style="font-weight: 600; color: var(--color-text);">BLE Discovery (unauthenticated)</div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Attacker scans for BLE GATT services, discovers server cert characteristic. No pairing or bonding required.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">2</div>
    <div style="font-weight: 600; color: var(--color-text);">Certificate Injection <span style="font-size: 0.8rem; font-weight: 400; color: var(--color-accent);">(Vuln 1)</span></div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Write attacker-controlled cert via GATT (<code>BT_GATT_PERM_WRITE</code>, no auth). Device stores it via <code>pouch_server_certificate_set()</code>.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">3</div>
    <div style="font-weight: 600; color: var(--color-text);">MITM Established</div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Device authenticates server using injected cert. Attacker holds the private key, intercepts and proxies the connection.</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">4</div>
    <div style="font-weight: 600; color: var(--color-text);">Exploitation <span style="font-size: 0.8rem; font-weight: 400; color: var(--color-accent);">(Vuln 2 or Vuln 3)</span></div>
  </div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin: 1rem 0 1.5rem;">
<div style="background: var(--color-bg-card); border: 1px solid #f59e0b; border-radius: 8px; padding: 1rem 1.25rem;">
<div style="font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem;">Memory Disclosure</div>
<div style="font-size: 0.8rem; font-weight: 600; color: #f59e0b; margin-bottom: 0.5rem;">Vuln 2</div>
<div style="font-size: 0.85rem; color: var(--color-text-secondary); line-height: 1.5;">Send 1-byte string response → <code>size_t</code> underflow → OOB read → leak adjacent heap memory (keys, creds)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid #dc2626; border-radius: 8px; padding: 1rem 1.25rem;">
<div style="font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem;">Remote Code Execution</div>
<div style="font-size: 0.8rem; font-weight: 600; color: #dc2626; margin-bottom: 0.5rem;">Vuln 3</div>
<div style="font-size: 0.85rem; color: var(--color-text-secondary); line-height: 1.5;">Send oversized integer response → <code>assert()</code> absent in release → <code>memcpy</code> overflows 12-byte stack buffer → overwrite return address → RCE</div>
</div>
</div>
<h3 id="step-by-step-walkthrough">Step-by-Step Walkthrough</h3>
<ol>
<li>
<p><strong>BLE Discovery</strong> (~10-100m range): The attacker uses a BLE scanner (e.g., <code>nRF Connect</code>, a custom <code>bleak</code> script) to discover the Golioth GATT service and the server certificate characteristic UUID.</p>
</li>
<li>
<p><strong>Certificate Injection</strong>: The attacker writes a valid, attacker-controlled X.509 certificate to the characteristic. Because <code>BT_GATT_PERM_WRITE</code> requires no authentication, this succeeds on first connection. The device calls <code>pouch_server_certificate_set()</code> and persists the certificate. No overflow is needed for this step. The certificate just needs to be within <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code>.</p>
</li>
<li>
<p><strong>MITM Established</strong>: 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 <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> to be disabled; with validation enabled (the default), the injected certificate is rejected against the embedded CA and the chain breaks here.</p>
</li>
<li>
<p><strong>Exploitation</strong>: From the MITM position, the attacker waits for, or triggers, a LightDB State query from the device, then:</p>
<ul>
<li><strong>For memory disclosure (Vuln 2)</strong>: 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.</li>
<li><strong>For code execution (Vuln 3)</strong>: Responds with an oversized payload to an integer GET. In release builds, the assert is gone, and <code>memcpy</code> overwrites the stack. On Cortex-M with no ASLR or stack canaries, the return address is overwritten with an attacker-controlled value.</li>
</ul>
</li>
</ol>
<h3 id="feasibility-and-limitations">Feasibility and Limitations</h3>
<ul>
<li><strong>BLE range constraint</strong>: The initial step requires physical proximity. In practice, directional antennas can extend BLE range significantly, and many IoT deployments are in physically accessible locations.</li>
<li><strong>Certificate persistence</strong>: Whether the injected certificate persists across reboots depends on the Pouch configuration and storage backend. If persistent, the MITM survives power cycles.</li>
<li><strong>Release build requirement</strong>: Vulnerability 3 requires that <code>NDEBUG</code> is defined. This is the default for production firmware on all supported platforms.</li>
<li><strong>Certificate validation</strong>: The MITM step requires <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> to be disabled. This option is enabled by default and validates server certificates against an embedded CA. When enabled, certificate injection is blocked.</li>
<li><strong>Vulnerability 4 is independent</strong>: The CoAP blockwise path issue is locally triggered and is not part of this chain.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>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.</p>
<p>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.</p>
<p>The bugs themselves are not exotic. They are <code>memcpy</code> without bounds checks, <code>assert</code> instead of <code>if</code>, <code>strncpy</code> 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.</p>
<p>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.</p>
<h2 id="disclosure-and-fix">Disclosure and Fix</h2>
<p>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 <a href="https://github.com/golioth/pouch/commit/1b2219a1" rel="noopener noreferrer" target="_blank">1b2219a1</a>.</p>
<h3 id="disclosure-timeline">Disclosure Timeline</h3>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Oct 31, 2025</div>
    <div style="color: var(--color-text);">SecMate sent initial report to Golioth (<code>security@golioth.io</code>) with findings and proof-of-concept code</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Nov 5, 2025</div>
    <div style="color: var(--color-text);">Golioth confirmed receipt and began internal review</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 7, 2026</div>
    <div style="color: var(--color-text);">SecMate followed up; noted 90-day disclosure window ending ~Jan 29, 2026</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 9, 2026</div>
    <div style="color: var(--color-text);">Golioth confirmed all reported vulnerabilities have been addressed</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 15, 2026</div>
    <div style="color: var(--color-text);">Golioth confirmed no CVEs assigned; agreed to end-of-January disclosure; SecMate offered to handle CVE assignment</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #f59e0b; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 16, 2026</div>
    <div style="color: var(--color-text);">CVE IDs assigned by <a href="https://vulncheck.com/">VulnCheck</a>: CVE-2026-23747, CVE-2026-23748, CVE-2026-23749, CVE-2026-23750</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 22, 2026</div>
    <div style="color: var(--color-text);">Version ranges provided to VulnCheck; CVE records finalized for Firmware SDK vulnerabilities</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 30, 2026</div>
    <div style="color: var(--color-text);">Public disclosure</div>
  </div>
</div>
<h3 id="cve-summary">CVE Summary</h3>
<div style="display: grid; gap: 0.75rem; margin: 1.5rem 0;">
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #dc2626 12%, transparent); color: #dc2626; border: 1px solid color-mix(in srgb, #dc2626 25%, transparent);">CVE-2026-23750</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">BLE GATT Server Cert Heap Overflow</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Pouch &middot; Introduced v0.1.0 &middot; Fixed in <a href="https://github.com/golioth/pouch/commit/1b2219a1" style="color: var(--color-accent);">1b2219a1</a></div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: #f59e0b; border: 1px solid color-mix(in srgb, #f59e0b 25%, transparent);">CVE-2026-23748</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">LightDB State String Underflow OOB Read</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.10.0 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/d7f55b38" style="color: var(--color-accent);">d7f55b38</a> (v0.22.0)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: #f59e0b; border: 1px solid color-mix(in srgb, #f59e0b 25%, transparent);">CVE-2026-23747</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">Assert-Guarded Memcpy Stack Overflow</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.10.0 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/57eac06" style="color: var(--color-accent);">57eac06</a> (v0.22.0)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: var(--color-muted-bg); color: var(--color-text-secondary); border: 1px solid var(--color-border);">CVE-2026-23749</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">CoAP Blockwise Unterminated Path OOB Read</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.19.1 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/0e788217" style="color: var(--color-accent);">0e788217</a> (v0.22.0)</div>
</div>
</div>
<h2 id="whats-next">What&rsquo;s Next</h2>
<p>This is our second public disclosure, after the <a href="/posts/libcoap-vulnerabilities-disclosure/">libcoap vulnerabilities</a> we published earlier. These four CVEs bring our public total to six, with more findings across embedded and systems projects still in coordinated disclosure.</p>
<p>For the full list, see our <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=golioth-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="golioth-vulnerabilities-disclosure" data-post-category="security">disclosure page</a>. If you&rsquo;re building on embedded systems and want to find vulnerabilities before attackers do, <a href="https://secmate.dev/register?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=golioth-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_register" data-post-slug="golioth-vulnerabilities-disclosure" data-post-category="security">reach out</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] Golioth. &ldquo;Golioth Firmware SDK&rdquo; <em>GitHub</em>. <a href="https://github.com/golioth/golioth-firmware-sdk" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Golioth. &ldquo;Golioth Pouch&rdquo; <em>GitHub</em>. <a href="https://github.com/golioth/pouch" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] IETF. &ldquo;RFC 7252 - The Constrained Application Protocol (CoAP)&rdquo; <em>IETF Datatracker</em>, June 2014. <a href="https://datatracker.ietf.org/doc/html/rfc7252" rel="noopener noreferrer" target="_blank">RFC</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] NVD. &ldquo;CVE-2026-23750 - BLE GATT Server Certificate Heap Overflow in Golioth Pouch&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23750" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] NVD. &ldquo;CVE-2026-23748 - LightDB State String Underflow OOB Read in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23748" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref6"></a>[6] NVD. &ldquo;CVE-2026-23747 - Assert-Guarded Memcpy Stack Overflow in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23747" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref7"></a>[7] NVD. &ldquo;CVE-2026-23749 - CoAP Blockwise Unterminated Path OOB Read in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23749" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item></channel></rss>