This is less a front-door break-in than a clerk overfilling a form and accidentally photocopying the next page
CVE-2026-34876 is an out-of-bounds read in mbedtls_ccm_finish() in Mbed TLS 3.1.0 through 3.6.5. The bug appears when software uses the *public multipart CCM API* and passes an oversized tag_len greater than 16, causing memcpy(tag, ctx->y, tag_len) to read past the 16-byte authentication buffer and leak adjacent CCM context data. Mbed TLS 3.6.6 fixes this; Mbed TLS 4.x had the same validation issue internally, but the function is not exposed through the supported public API there.
The vendor's HIGH/7.5 score reflects the theoretical CVSS view that an attacker may be remote and unauthenticated. In real enterprise deployments, that overstates the risk: the attacker still needs an application that exposes the raw multipart CCM interface to attacker-controlled input, while the unaffected one-shot CCM APIs and PSA/cipher wrappers cover a lot of normal usage. This is a real bug worth fixing, but it is *not* a generic pre-auth network break across everyone who links Mbed TLS.
4 steps from start to impact.
Find a reachable Mbed TLS 3.x embedding
- Target software embeds Mbed TLS 3.1.0-3.6.5
- Target feature uses CCM mode
- The embedding application exposes that feature to untrusted input
- Mbed TLS is a library, so internet exposure depends entirely on the parent application
- Many products use TLS/X.509 paths, not raw multipart CCM
- Vendor advisory states one-shot CCM APIs and PSA/cipher wrappers are not affected
Reach the raw multipart API path
mbedtls_ccm_finish(), which is only exploitable when the application allows attacker-influenced flow into the multipart CCM sequence. In practice that means a custom protocol handler, embedded product, or application wrapper that accepts or derives a malicious tag_len from untrusted input.- Application directly uses the public multipart CCM API
- Attacker can influence or set
tag_len - Application does not validate
tag_lenbefore calling the library
- Most sane wrappers hardcode or negotiate tag length once, instead of letting users pick it arbitrarily
- Modern secure coding review or fuzzing often catches attacker-controlled length fields near crypto code
- The vulnerable function is not a typical externally exposed API surface
mbedtls_ccm_finish() and mismatched mbedtls_ccm_set_lengths() usage; network scanners generally cannot.Trigger oversized copy
tag_len above 16, the vulnerable code executes memcpy(tag, ctx->y, tag_len). That reads past ctx->y and discloses up to tag_len - 16 bytes of adjacent CCM context, including nonce/counter data, length fields, mode parameters, and potentially key-dependent block cipher state.tag_lenexceeds 16- The call path reaches
mbedtls_ccm_finish() - The application returns or otherwise exposes the resulting tag buffer
- Leak size is bounded and tied to adjacent structure layout
- This is read-only disclosure, not memory corruption for write or control flow
- If the application discards or masks the returned tag, exfiltration value drops sharply
Convert leaked bytes into useful data
ctx->y in the compiled structure and whether the attacker can repeat the read. In the best attacker case, repeated requests expose cryptographic context that helps session analysis or secret recovery; in many real deployments, it leaks only low-value state and stops there.- Leaked bytes are returned to attacker-visible output
- Adjacent memory contains sensitive context
- Attacker can repeat the operation reliably
- No direct code execution or memory modification primitive
- Per-adaptation blast radius is usually limited to the process or session using that CCM context
- Exploit utility varies heavily by compiler, layout, and application behavior
The supporting signals.
| In-the-wild status | No current evidence of active exploitation in reviewed sources. CISA KEV does not list this CVE, and the vendor advisory does not claim exploitation. |
|---|---|
| Proof-of-concept availability | No public PoC located in reviewed primary sources. That is an inference from the source set, not a guarantee that no private exploit exists. |
| EPSS | 0.00026 (~0.026%) from the user-supplied intel, which is very low and consistent with a narrow, hard-to-reach library flaw. Percentile was not provided in the source set reviewed here. |
| KEV status | Not listed in the CISA KEV catalog as of this review. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N assumes network reachability and attacker control are present. The *real* gating factor is that the application must expose the raw multipart CCM API with attacker-controlled tag_len. |
| Affected versions | Mbed TLS 3.1.0 through 3.6.5 are affected upstream. Vendor notes that 4.x had the same validation problem internally, but mbedtls_ccm_finish() is not exposed via the public API there. |
| Fixed versions | Upstream fix is Mbed TLS 3.6.6. Debian tracking shows forky/sid fixed at 3.6.6-0.1, while bookworm/trixie remained vulnerable in the reviewed tracker snapshot. |
| Exposure population | No reliable internet-wide host count: this is a library/API flaw, not a bannerable standalone service. Shodan/Censys-style host counting is low-signal here because exposure is determined by the embedding application. |
| Disclosure and reporter | Vendor advisory date is 2026-03-31; NVD published on 2026-04-02. Credit goes to Eva Crystal (0xiviel). |
noisgate verdict.
The decisive downward pressure is application reachability: exploitation requires a product to expose the raw multipart CCM API with attacker-controlled tag_len, which sharply narrows the real-world population versus the vendor's network-reachable CVSS baseline. This is a legitimate confidentiality bug, but it is not a broadly wormable or one-request internet compromise class issue.
Why this verdict
- Downgrade for attacker-position friction: vendor CVSS starts at unauthenticated remote, but in reality the attacker needs a product that *already* exposes raw multipart CCM semantics to untrusted input, which many deployments never do.
- Downgrade for exposure fraction: this is a library bug, not an exposed appliance endpoint. The reachable population is the subset of Mbed TLS consumers using multipart CCM *and* passing attacker-controlled
tag_lenunsafely. - Downgrade for security-tool intercepts: SAST, fuzzing, secure wrappers, and ordinary application input validation are all modern controls that can stop the vulnerable call chain before exploitation.
- Keep at MEDIUM, not LOW: if the bad call path exists, triggering it is easy and the leak can expose sensitive CCM context, including key-dependent cipher state.
Why not higher?
This is not a generic pre-auth compromise of every host with Mbed TLS installed. The exploit chain depends on multiple narrowing prerequisites: the vulnerable 3.x branch, actual multipart CCM usage, attacker influence over tag_len, and application behavior that returns leaked bytes usefully. There is also no evidence in the reviewed sources of KEV listing, active exploitation, or public weaponization.
Why not lower?
The bug is still a real memory disclosure in security-sensitive cryptographic code, not a theoretical lint finding. If you ship or run custom embedded software that directly uses mbedtls_ccm_finish() on attacker-shaped inputs, the vulnerability is straightforward to trigger and can leak cryptographic context that should never leave process memory.
What to do — in priority order.
- Block raw multipart CCM exposure — Do not expose the multipart CCM API to untrusted callers; route callers through one-shot CCM APIs or PSA/cipher wrappers instead. For a MEDIUM verdict there is no mitigation SLA, but this is the right engineering guardrail to apply as code is touched before the 365-day remediation window closes.
- Clamp
tag_lenat the boundary — Enforce CCM tag sizes of 4-16 bytes and even values only, and requirembedtls_ccm_finish()to use the sametag_lenpreviously set bymbedtls_ccm_set_lengths(). For a MEDIUM verdict there is no mitigation SLA — go straight to the 365-day remediation window, but this check is a solid temporary control for products that cannot upgrade immediately. - Inventory embedded copies — Use SBOM, package inventory, firmware BOMs, and source scans to find Mbed TLS 3.1.0-3.6.5 hiding inside appliances, containers, and statically linked binaries. This matters more than perimeter scanning because the affected component is usually buried inside another product.
- Add targeted fuzz tests — Fuzz multipart CCM setup and finish paths with malformed and oversized
tag_lenvalues in CI. That catches this bug class and future length-validation mistakes before deployment.
- A WAF does not fix this by itself, because the bug is inside application/library handling after the request has already been accepted and translated into a crypto call.
- MFA is irrelevant unless the vulnerable feature is behind authentication *and* authentication fully prevents attacker access to the code path; the bug itself is not an identity problem.
- Version-only internet scanning is insufficient because finding Mbed TLS in a package list does not tell you whether multipart CCM is reachable from untrusted input.
Crowdsourced verification payload.
Run this on the target host or inside the target container/firmware build environment where Mbed TLS is installed. Invoke it as python3 check_cve_2026_34876.py for auto-detection, or python3 check_cve_2026_34876.py --version 3.6.5 / python3 check_cve_2026_34876.py --path /usr/lib/x86_64-linux-gnu/libmbedcrypto.so; no special privileges are required unless your library paths are restricted.
#!/usr/bin/env python3
# check_cve_2026_34876.py
# Detect likely exposure to CVE-2026-34876 (Mbed TLS 3.1.0 through 3.6.5)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
import argparse
import os
import platform
import re
import shutil
import subprocess
import sys
from typing import Optional, Tuple
VULN_MIN = (3, 1, 0)
VULN_MAX_EXCL = (3, 6, 6)
def normalize_version(raw: str) -> Optional[Tuple[int, int, int]]:
if not raw:
return None
m = re.search(r'(\d+)\.(\d+)\.(\d+)', raw)
if not m:
return None
return tuple(int(x) for x in m.groups())
def classify(ver: Tuple[int, int, int]) -> str:
if VULN_MIN <= ver < VULN_MAX_EXCL:
return 'VULNERABLE'
if ver >= (4, 0, 0):
return 'PATCHED'
if ver < VULN_MIN:
return 'PATCHED'
return 'UNKNOWN'
def run(cmd):
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
if p.returncode == 0:
return p.stdout.strip()
except Exception:
pass
return ''
def try_pkg_config() -> Optional[str]:
if shutil.which('pkg-config'):
for name in ('mbedtls', 'mbedtls3', 'libmbedtls'):
out = run(['pkg-config', '--modversion', name])
if out:
return out
return None
def try_dpkg() -> Optional[str]:
if shutil.which('dpkg-query'):
pkgs = ['libmbedtls-dev', 'libmbedtls14', 'libmbedtls', 'mbedtls']
for pkg in pkgs:
out = run(['dpkg-query', '-W', '-f=${Version}', pkg])
if out:
return out
return None
def try_rpm() -> Optional[str]:
if shutil.which('rpm'):
for pkg in ('mbedtls', 'mbedtls-devel', 'libmbedtls'):
out = run(['rpm', '-q', '--qf', '%{VERSION}-%{RELEASE}', pkg])
if out and 'not installed' not in out.lower():
return out
return None
def try_apk() -> Optional[str]:
if shutil.which('apk'):
out = run(['apk', 'info', '-e', 'mbedtls'])
if out:
out2 = run(['apk', 'info', '-v', 'mbedtls'])
return out2 or out
return None
def try_pacman() -> Optional[str]:
if shutil.which('pacman'):
out = run(['pacman', '-Q', 'mbedtls'])
if out:
return out
return None
def try_brew() -> Optional[str]:
if shutil.which('brew'):
out = run(['brew', 'list', '--versions', 'mbedtls'])
if out:
return out
return None
def try_strings(path: str) -> Optional[str]:
if not path or not os.path.exists(path):
return None
if shutil.which('strings'):
out = run(['strings', '-a', path])
if out:
for line in out.splitlines():
if 'mbed TLS ' in line or 'Mbed TLS ' in line or 'mbedtls-' in line:
m = re.search(r'(\d+\.\d+\.\d+)', line)
if m:
return m.group(1)
return None
def guess_library_paths():
paths = []
sysname = platform.system().lower()
if sysname == 'linux':
paths.extend([
'/usr/lib/libmbedcrypto.so',
'/usr/lib64/libmbedcrypto.so',
'/usr/lib/x86_64-linux-gnu/libmbedcrypto.so',
'/usr/local/lib/libmbedcrypto.so',
'/lib/x86_64-linux-gnu/libmbedcrypto.so',
])
elif sysname == 'darwin':
paths.extend([
'/opt/homebrew/lib/libmbedcrypto.dylib',
'/usr/local/lib/libmbedcrypto.dylib',
])
elif sysname == 'windows':
paths.extend([
r'C:\\Program Files\\Mbed TLS\\libmbedcrypto.dll',
r'C:\\mbedtls\\libmbedcrypto.dll',
])
return [p for p in paths if os.path.exists(p)]
def main():
parser = argparse.ArgumentParser(description='Check likely exposure to CVE-2026-34876')
parser.add_argument('--version', help='Explicit Mbed TLS version to evaluate, e.g. 3.6.5')
parser.add_argument('--path', help='Path to libmbedcrypto library/binary to inspect with strings')
args = parser.parse_args()
evidence = []
if args.version:
evidence.append(('explicit', args.version))
if args.path:
v = try_strings(args.path)
if v:
evidence.append((args.path, v))
for label, func in [
('pkg-config', try_pkg_config),
('dpkg', try_dpkg),
('rpm', try_rpm),
('apk', try_apk),
('pacman', try_pacman),
('brew', try_brew),
]:
v = func()
if v:
evidence.append((label, v))
for p in guess_library_paths():
v = try_strings(p)
if v:
evidence.append((p, v))
seen = []
for src, raw in evidence:
ver = normalize_version(raw)
if ver:
seen.append((src, raw, ver, classify(ver)))
if not seen:
print('UNKNOWN - could not determine installed Mbed TLS version or library path')
sys.exit(2)
# Prefer any vulnerable finding over patched, because fleets often have multiple copies.
vuln = [x for x in seen if x[3] == 'VULNERABLE']
if vuln:
src, raw, ver, state = vuln[0]
print(f'VULNERABLE - detected Mbed TLS {ver[0]}.{ver[1]}.{ver[2]} from {src} ({raw})')
sys.exit(1)
patched = [x for x in seen if x[3] == 'PATCHED']
if patched:
src, raw, ver, state = patched[0]
print(f'PATCHED - detected Mbed TLS {ver[0]}.{ver[1]}.{ver[2]} from {src} ({raw})')
sys.exit(0)
print('UNKNOWN - version detected but could not classify confidently')
sys.exit(2)
if __name__ == '__main__':
main()
If you remember one thing.
tag_len or route callers to unaffected one-shot APIs immediately while scheduling the upstream fix. Apply the actual vendor patch to 3.6.6 or later within the noisgate remediation SLA of 365 days.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.