GHSA-hffm-g8v7-wrv7
Caddy: mTLS client authentication silently fails open when CA certificate file is missing or malformed
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
github.com/caddyserver/caddy/v2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Two swallowed errors in ClientAuthentication.provision() cause mTLS client certificate authentication to silently fail open when a CA certificate file is missing, unreadable, or malformed. The server starts without error but accepts any client certificate signed by any system-trusted CA, completely bypassing the intended private CA trust boundary.
Details
In modules/caddytls/connpolicy.go, the provision() method has two return nil statements that should be return err:
Bug #1 — line 787:
ders, err := convertPEMFilesToDER(fpath)
if err != nil {
return nil // BUG: should be "return err"
}
Bug #2 — line 800:
err := caPool.Provision(ctx)
if err != nil {
return nil // BUG: should be "return err"
}
Compare with line 811 which correctly returns the error:
caRaw, err := ctx.LoadModule(clientauth, "CARaw")
if err != nil {
return err // CORRECT
}
When the error is swallowed on line 787, the chain is:
TrustedCACertsremains empty (no DER data appended from the file)- The
len(clientauth.TrustedCACerts) > 0guard on line 794 is false — skipped clientauth.CARawis nil — line 806 returns nilclientauth.caremains nil — no CA pool was createdprovision()returns nil — caller thinks provisioning succeeded
Then in ConfigureTLSConfig():
Active()returns true becauseTrustedCACertPEMFilesis non-empty- Default mode is set to
RequireAndVerifyClientCert(line 860) - But
clientauth.cais nil, socfg.ClientCAsis never set (line 867 skipped) - Go's
crypto/tlswithRequireAndVerifyClientCert+ nilClientCAsverifies client certs against the system root pool instead of the intended CA
The fix is changing return nil to return err on lines 787 and 800.
PoC
- Configure Caddy with mTLS pointing to a nonexistent CA file:
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":443"],
"tls_connection_policies": [{
"client_authentication": {
"trusted_ca_certs_pem_files": ["/nonexistent/ca.pem"]
}
}]
}
}
}
}
}
-
Start Caddy — it starts without any error or warning.
-
Connect with any client certificate (even self-signed):
openssl s_client -connect localhost:443 -cert client.pem -key client-key.pem
- The TLS handshake succeeds despite the certificate not being signed by the intended CA.
A full Go test that proves the bug end-to-end (including a successful TLS handshake with a random self-signed client cert) is here: https://gist.github.com/moscowchill/9566c79c76c0b64c57f8bd0716f97c48
Test output:
=== RUN TestSwallowedErrorMTLSFailOpen
BUG CONFIRMED: provision() swallowed the error from a nonexistent CA file.
tls.Config has RequireAndVerifyClientCert but ClientCAs is nil.
CRITICAL: TLS handshake succeeded with a self-signed client cert!
The server accepted a client certificate NOT signed by the intended CA.
--- PASS: TestSwallowedErrorMTLSFailOpen (0.03s)
Impact
Any deployment using trusted_ca_cert_file or trusted_ca_certs_pem_files for mTLS will silently degrade to accepting any system-trusted client certificate if the CA file becomes unavailable. This can happen due to a typo in the path, file rotation, corruption, or permission changes. The server gives no indication that mTLS is misconfigured.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/caddyserver/caddy/v2 | all versions | 2.11.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/caddyserver/caddy/v2. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update github.com/caddyserver/caddy/v2 to 2.11.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-hffm-g8v7-wrv7 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-hffm-g8v7-wrv7 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-hffm-g8v7-wrv7. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-hffm-g8v7-wrv7 in your dependencies?
O3 detects GHSA-hffm-g8v7-wrv7 across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.