FoxIO published a set of three TLS bugs in GoAhead this week (blog October 26, advisories October 12), and the middle one is the kind of bug I cannot resist checking with arithmetic. GoAhead is the embedded web server inside a long list of IoT, firewall, printer, and industrial devices. Its Mbed TLS backend configures the RFC 3526 2048-bit Diffie-Hellman group like this, paraphrasing FoxIO's writeup:
mbedtls_ssl_conf_dh_param_bin(ctx,
dhm_p, sizeof(dhm_g), // <-- length of p, taken from g
dhm_g, sizeof(dhm_g));
dhm_g is the generator: one byte. So the length passed for dhm_p — a 256-byte prime — is also one byte. Mbed TLS dutifully reads only the first byte of the prime and throws the other 255 bytes away.
The first byte of the RFC 3526 2048-bit prime is 0xFF. So the "2048-bit DH exchange" runs with modulus 255.
I counted what is left
You do not need a lab to see how bad this is. With p = 255 and generator g = 2, the shared secret is 2^x mod 255 for some exponent x. Here is the entire universe of possible outcomes:
p_trunc = 0xFF # 255: first byte of the RFC 3526 2048-bit prime
g = 2
vals = sorted({pow(g, x, p_trunc) for x in range(1, 5000)})
print(vals, len(vals))
print([pow(2, i, 255) for i in range(9)])
[1, 2, 4, 8, 16, 32, 64, 128] 8
[1, 2, 4, 8, 16, 32, 64, 128, 1]
Eight values. The powers of two cycle mod 255 with period 8, because 2^8 = 256 ≡ 1 (mod 255). A 2048-bit DH exchange is supposed to leave an observer facing roughly 2^2048 possibilities. This one leaves 3 bits of entropy.
Then I simulated the passive attack FoxIO describes — record the handshake, enumerate the candidates, keep the one that decrypts:
attacker attempts per session (20 sessions): [1, 3, 1, 7, 7, 6, 6, 3, 7, 4, 2, 1, 6, 2, 7, 8, 7, 3, 1, 5]
mean attempts: 4.35 max: 8
FoxIO's own proof of concept recovered the shared secret "after five attempts." My simulation says the expected value is 4.5 (the mean of 1 through 8) and I measured 4.35 over 20 sessions. The bug and the math agree, which is the depressing part: there was nothing left to get wrong.
The fine print that decides whether you care
Per FoxIO, this one is GOAHEAD-SA-2026-0005, CVSS 5.9, affecting GoAhead 4.0.2 through 6.1.0 on the Mbed TLS backend, and only when all of these hold:
- TLS 1.2 with a finite-field
DHE_RSAsuite is negotiated (ECDHE and TLS 1.3 are unaffected, and Mbed TLS 4.x removed these suites entirely). - The client accepts the broken parameters. FoxIO notes the tested OpenSSL client refused them with "modulus too small" — permissive or embedded clients are the ones that complete a decryptable session.
- The typo dates to 2018, from the migration to the binary-parameter API, and survived an April 2024 TLS refactor. Fixed in GoAhead 6.1.1 (released August 2); upgrade, disable finite-field DHE suites, or use the OpenSSL backend.
The other two bugs in the same post are configuration-shaped in the same way, and arguably worse: on OpenSSL-compatible builds (CVSS 9.1), a configured CRL is loaded into the store but revocation checking is never enabled, so revoked client certificates still complete the mTLS handshake; on LibreSSL legacy-verifier builds (CVSS 7.4), an over-depth chain is reported as a tolerated issuer error instead of a depth error, so the chain-depth limit of 10 is not enforced when issuer checking is disabled.
The pattern
Three bugs, three different TLS libraries, one shape: GoAhead asked the library for a security property — check this revocation list, use this DH group, enforce this depth — and in each case the request silently did less than it said. Nothing failed loudly. The CRL loaded without error, the DH parameter was accepted, the handshake completed and returned HTTP 200. Every one of these systems "worked" for years, in one case since 2018.
That is the part worth internalizing if you integrate crypto libraries for a living: a loaded CRL is not a checked CRL, and a 256-byte buffer passed with a 1-byte length is a 1-byte number. The verification step is not "did the API call succeed." It is "show me the ServerKeyExchange bytes" — FoxIO's tell was right there on the wire, 00 01 ff, a one-byte modulus where 256 bytes should be. Nobody looked, because the handshake succeeded.
Sources: FoxIO, "Choose Your Backend: Three TLS Bugs in GoAhead" (Oct 26, 2026), https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead — all version ranges, CVSS scores, and configuration conditions above are theirs; the modular arithmetic, the 8-candidate enumeration, and the 20-session attempt simulation are mine, run locally.
Top comments (0)