I have a soft spot for embedded web servers, because they are the last place where one missing function call can outlive the person who forgot it. GoAhead is 115K of C that device makers compile into router, printer, firewall, gateway and industrial controller firmware. On October 26, Sébastien Féry of FoxIO published a write-up on three TLS bugs in it, all found in April and all fixed in GoAhead 6.1.1, which EmbedThis handed to device builders on August 2 and documented publicly on October 12.
The three findings share a shape I find hardest to defend against: the security control was configured, its inputs were correct, the call returned success, and nothing made the result count. The affected range on the worst one starts at 3.4.5, so the exposure window is measured in years.
🔒 Three Bugs, Three Backends, One Pattern
GoAhead lets you pick a TLS backend at build time, and which backend you picked decides which bug you have:
- OpenSSL-compatible backend, CVE-2026-115881 (GOAHEAD-SA-2026-0006, GHSA-5m75-xq69-c3c5). GoAhead 3.4.5 through 6.1.0. EmbedThis rates it 9.1. The CRL configured through
ME_GOAHEAD_SSL_REVOKEis loaded into the certificate store and never consulted, so a revoked client certificate completes the mTLS handshake as if it were valid. - Mbed TLS backend, GOAHEAD-SA-2026-0005 (GHSA-fmvx-2xfx-2gq4), no CVE. GoAhead 4.0.2 through 6.1.0. Rated 5.9. A length typo sends a one-byte Diffie-Hellman modulus instead of the full 2048-bit prime, so a client that accepts the parameters negotiates a session a passive observer can decrypt.
- LibreSSL legacy verifier, CVE-2026-115884 (GOAHEAD-SA-2026-0004, GHSA-r392-vqwx-3r4g). Builds linked against LibreSSL 3.2.x or 3.3.2 through 3.3.6. Rated 7.4. A client certificate chain longer than the ten-certificate limit is accepted when issuer checking is off.
CVE-2026-115881 and CVE-2026-115884 are the identifiers EmbedThis published, but as of October 27 they were not yet live in the public CVE and NVD databases. The severity numbers are the vendor’s own. Treat the ratings as provisional and the version ranges as the part you can act on.
🧩 Loaded Is Not Enforced
The critical one deserves the detail, because it is the cleanest example of the pattern I keep running into.
GoAhead’s OpenSSL backend loads the CRL in sslOpen(). It fetches the certificate store, it calls X509_STORE_load_locations(), and it aborts initialization if the load fails. Every line of that block is reasonable. What it does not do is call X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK), the flag OpenSSL requires before its check_revocation() will look at the list. Without that flag, check_revocation() hard early-returns success on every handshake without opening the CRL.
So the file loads, the startup path emits no warning, and the operator who deliberately configured ME_GOAHEAD_SSL_REVOKE has every reason to believe revocation is working. It is not. In FoxIO’s test against GoAhead 6.1.0 with OpenSSL 3.6.2, a revoked certificate signed by a trusted CA completed the handshake with issuer verification on, and the verify callback reported preverify_ok=1 error=0(ok) at every depth. The callback itself is correct. Its default: branch would reject X509_V_ERR_CERT_REVOKED (error 23). OpenSSL never produces that error, so the branch never runs.
That is the whole bug: one line of glue between a configuration file and a verification path, missing across versions 3.4.5 through 6.1.0 and every OpenSSL and LibreSSL build in that range. The defect is not OpenSSL-version-specific, which is the tell that it lives in GoAhead’s integration rather than in the library.
For anyone running a fleet of mTLS devices, revoking a certificate is the only remote kill switch for a stolen or decommissioned unit; the alternative is shipping firmware and hoping the owner applies it. On an affected build, that kill switch was decorative.
🔑 The Login Bypass Has the Same Shape
The TLS bugs are three of 11 advisories closed in 6.1.1, and three of them are critical. The other critical one is worse in practice and follows the same logic.
GOAHEAD-SA-2026-0001 affects HTTP Basic and form login. If a user’s password is stored as a Blowfish (BF1:) hash, a format GoAhead added in 6.1.0, anyone who knows a valid username can sign in with any password. The researcher’s disclosure shows the mechanism: the code computes the correct password comparison and then never uses the result. Computed, discarded.
The rest of the set is standard embedded server misery. GOAHEAD-SA-2026-0007 runs .bat CGI scripts through cmd.exe with request data on the command line, which is command injection on Windows builds. There are three HTTP request-smuggling variants, two more CGI injection issues, a Digest authentication nonce that leaked the server’s master secret, and session fixation. The release came out of a full in-house source audit that also closed 31 internally found defects.
EmbedThis is direct that this is the end of the line: GoAhead is in maintenance mode, and new projects are steered toward its Ioto device agent. A 115K server that EmbedThis itself describes as deployed in hundreds of millions of devices is now in bugfix-only mode, which is a strange place for the internet’s router management pages to be.
📉 Scope: The Device Count Is Marketing, the Config Is the Story
Every headline about this said hundreds of millions of devices. That number comes from EmbedThis’s own product overview, not from any census, and it counts devices that ship GoAhead, not devices that ship a vulnerable configuration. FoxIO’s CTO John Althouse named Palo Alto Networks, Comcast, D-Link, HP, Canon, Honeywell, GE, Oracle, Siemens, Rockwell, Digi, Lockheed Martin and Boeing among the companies whose products use the server. None of those vendors has been shown to ship an affected build, and as of October 28 none had published an advisory on them.
The critical bug needs all of the following at once: the OpenSSL backend, ME_GOAHEAD_SSL_VERIFY_PEER=1, a CA bundle, ME_GOAHEAD_SSL_VERIFY_ISSUER=1, a configured CRL, and an attacker holding a certificate that a trusted CA issued and that the operator already revoked. The default build has no mTLS and no CRL. CRL-configured deployments are the population at risk, and inside that population every affected version is exploitable on every handshake until upgrade.
The LibreSSL depth bug deserves the same honesty. In the configuration it needs, VERIFY_ISSUER=0, ordinary self-signed certificates are already accepted by policy. What the over-depth chain buys an attacker is mostly the depth limit itself, not trust. FoxIO says as much in the write-up, and the fix hardens the callback regardless of backend as defense in depth.
And the mbed TLS bug only lands if the client accepts an eight-bit DH group. OpenSSL, current Go, current Java and mainstream browsers refuse a modulus that small and fall back to ECDHE, so the session never completes. Custom and embedded clients without a minimum-size check do accept it, which is exactly the peer population these servers tend to talk to.
No exploitation of any of the three has been reported. Given that all three bugs let the handshake complete normally, absence of reports is weak evidence. Nobody would see this in a log.
🛠️ What I Would Actually Test
The reason these bugs survived is not carelessness. It is that every one of them is invisible from the outside and silent at runtime. So the tests have to be negative tests, and they have to fail loudly:
- Assert the revocation path. Generate a client cert, add it to a CRL, point the build at that CRL, and require the handshake to fail with
error=23. If it succeeds, the flag is missing. An afternoon of work that would have caught a 9.1. - Assert the wrong password fails. For the BF1 path, one request with a valid username and a garbage password. If it returns 200, stop shipping.
- Watch startup warnings. A CRL that loads without a warning proves nothing on this codebase. Log the effective verification flags at boot and assert that
X509_V_FLAG_CRL_CHECKis set. - Find the fleet before you patch. Device owners cannot tell from outside which backend or verification settings a build uses. FoxIO published JA4Scan fingerprints that map GoAhead 6.1.x servers to OpenSSL, LibreSSL and mbed TLS builds, plus proof-of-concept code. For the mbed TLS bug, check JA4S server fingerprints for DHE_RSA suites such as
0x009Fand0xCCAA; a match liket120100_009f_1ca028f07214suggests a session that negotiated through the broken group. - Interim options, where they exist. Mbed TLS builds can disable finite-field DHE suites or move to OpenSSL. LibreSSL builds can enable issuer verification with a trusted CA bundle, or move to LibreSSL 4.x or OpenSSL. The CRL advisory has no workaround: upgrade to 6.1.1.
- Plan for the firmware lag. An upstream patch puts nothing on a device. Each manufacturer has to rebuild and ship, and the release notes tell operators to invalidate sessions, restart, and rotate any BF1-stored passwords after upgrading, because the old hashes were effectively public.
🧠 Final Thoughts
The interesting thing here is not that an embedded web server had TLS bugs. It is that four separate controls in one codebase were configured, computed, or loaded, and then had their results ignored. The CRL was loaded and not read. The password comparison was computed and discarded. The DH prime was declared at 2048 bits and truncated to one byte. The depth limit was enforced only when no other error existed.
That is the failure mode of security code that never gets exercised in the failure direction. Positive tests confirm the happy path, which was never the problem. If you maintain an embedded TLS stack, the test that matters is the one where a legitimate-looking peer must be rejected, because the bugs that live in this class produce no error, no log line, and a completed handshake.
Thanks for reading.