Skip to content

Built-in TLS: CA-bundle certificate chain accepted without any signature verification

High
scaprile published GHSA-qj6j-2692-v2r8 Aug 12, 2026

Package

No package listed

Affected versions

7.22

Patched versions

7.23

Description

Built-in TLS: CA-bundle certificate chain accepted without any signature verification

Severity: HIGH

Location

  • File: src/tls_builtin.c
  • Line: 1890

Description

When a client is configured with a multi-certificate PEM CA bundle (mg_tls_opts.ca containing more than one CERTIFICATE block), mg_tls_init() stores it in tls->ca_bundle_der and leaves tls->ca_der.len == 0. In mg_tls_recv_cert(), the intra-chain loop only verifies that cert[i-1] was signed by cert[i]; the last (topmost) certificate of the presented chain is never verified against a trust anchor. The bundle fallback block at lines 1887-1897 only calls tls_bundle_find(tls, &certs[certnum-1].issuer, &ca), which merely searches the bundle for a certificate whose subject Common Name string equals the issuer Common Name of the last chain certificate (mg_strcasecmp on CN only), and then does nothing with the result. The signature check that exists for the single-CA case (mg_tls_verify_cert_signature(&certs[certnum-1], &ca) at lines 1898-1906) is guarded by tls->ca_der.len > 0, which is always false in bundle mode. Consequently, trust-anchor validation degenerates into a string comparison of a CN.

Attack Scenario

A network attacker (DNS/ARP spoofing, hostile Wi-Fi, BGP hijack, or any MITM position) intercepts a Mongoose HTTPS/MQTTS/WSS client connection that was configured with a CA bundle. The attacker generates a self-signed certificate whose subject CN/SAN matches the target hostname (satisfying mg_tls_verify_cert_san()/mg_tls_verify_cert_cn()) and whose issuer Common Name is set to the CN of any root present in the victim's bundle (e.g. "ISRG Root X1" or "DigiCert Global Root CA"). It presents this single certificate in the TLS Certificate message and signs the CertificateVerify with its own private key. mg_tls_recv_cert() finds a bundle entry with a matching subject CN, reports success, and mg_tls_recv_cert_verify() validates the CertificateVerify against the attacker's own public key taken from that same forged certificate.

Impact

Complete TLS server-authentication bypass for built-in-TLS clients using a CA bundle: full man-in-the-middle interception and modification of all traffic, disclosure of credentials/tokens/telemetry, and injection of attacker-controlled responses (including, on devices that use HTTP OTA, attacker-controlled firmware).

Recommendation

In the !found_ca && tls->ca_bundle_len > 0 branch, after tls_bundle_find() locates a candidate anchor, cryptographically verify the last chain certificate against it, e.g. if (!mg_tls_verify_cert_signature(&certs[certnum - 1], &ca)) { mg_error(c, "failed to verify CA"); return -1; }, and additionally require that the anchor is a CA (ca.is_ca). Matching on issuer/subject Common Name must never by itself be treated as chain validation; prefer matching by full issuer DN plus Subject Key Identifier and always finish with a signature check.

Evidence

mg_tls_init(): if (cert_count > 1 && c->is_client) { tls->ca_bundle_len = cert_count; tls->ca_bundle_der = all_certs; } leaves ca_der.len == 0. mg_tls_recv_cert(): if (!found_ca && certnum > 0 && tls->ca_bundle_len > 0) { r = tls_bundle_find(tls, &certs[certnum-1].issuer, &ca); if (r <= 0) { mg_error(...); return -1; } MG_VERBOSE(("CA serial: ...")); } — no mg_tls_verify_cert_signature() call. tls_bundle_find() returns 1 purely on mg_strcasecmp(subj, tgt) == 0 where subj/tgt are OID 2.5.4.3 (CN) values.

Severity

High

CVE ID

CVE-2026-73251

Weaknesses

No CWEs

Credits