EEF-CVE-2026-117666
Dashboard / Vulnerabilities / EEF-CVE-2026-117666
EEF-CVE-2026-117666
Summary: ssl_verify_fun: partial-label wildcard certificate matches hostnames across a label boundary
Details: ## Summary Improper Validation of Certificate with Host Mismatch vulnerability in deadtrickster ssl_verify_fun allows an adversary in the middle to impersonate a TLS peer via a certificate that carries a partial-label wildcard. `try_match_wildcard/4` in `src/ssl_verify_hostname.erl` anchors only the text before and after the `*` of an identifier such as `f*.example.net`, so the wildcard also consumes dots and the certificate is accepted for `f.evil.example.net`. RFC 6125 confines a wildcard to the left-most label. The same matcher lets `*.168.1.1` match the IP literal `192.168.1.1`. Whole-label wildcards are matched correctly. Erlang/OTP 20.0 and later check the hostname in `ssl` before a user `verify_fun` sees `valid_peer`, so a client that keeps `server_name_indication` set rejects the mismatch regardless. Exposed are clients that disable SNI, servers that verify client certificate hostnames with this library, and direct callers of `verify_cert_hostname/2`. This issue affects ssl_verify_fun: from 1.0.1 before 1.1.8. ## Details **1. Identifier parsing.** `ssl_verify_hostname:try_match_hostname/2` strips trailing dots, tries an exact case-insensitive match, and otherwise calls `parse_and_validate_wildcard_identifier/2`. That function accepts an identifier with one `*` in the left-most label and at least two labels after it, and classifies it as `SingleCharW` when the label is exactly `*`. **2. Whole-label wildcard, correct.** For `*.example.net` the matcher compares the text after the `*` with the hostname from its first dot onward, so `a.b.example.net` fails because `.b.example.net` differs from `.example.net`. **3. Partial-label wildcard, incorrect.** For `f*.example.net` the `SingleCharW` branch is skipped. `try_match_wildcard/4` then checks only that the hostname ends with `.example.net` and starts with `f`. Nothing checks the span consumed by the `*`, so `f.evil.example.net` and `f.a.b.example.net` pass. RFC 6125 section 6.4.3 limits a wildcard to the left-most label. The `wildcard_not_in_a_label/2` guard only rejects internationalized `xn--` labels and does not help. **4. IP literals.** The reference identifier is treated as a DNS name whatever its shape, so `verify_cert_hostname/2` with hostname `192.168.1.1` matches a `dNSName` of `*.168.1.1` through the whole-label branch. RFC 6125 does not apply wildcard matching to IP addresses, and OTP's `ssl` converts an IP-literal string to an `{ip, ...}` reference that only matches `iPAddress` entries. **5. Interaction with Erlang/OTP.** When `ssl_verify_hostname:verify_fun/3` is used as `verify_fun`, OTP 20.0 and later call `ssl_certificate:validate/4` for `valid_peer` before the user fun. In the client role with `server_name_indication` set to a hostname, or with a peer IP from a socket upgrade, OTP runs `public_key:pkix_verify_hostname/3` and hands the user fun `{bad_cert, hostname_check_failed}` instead of `valid_peer`, which ssl_verify_fun turns into `{fail, ...}`. OTP's default match function allows no wildcards, and its `https` match function (`public_key:pkix_verify_hostname_match_fun/1`) splits both names on dots and matches the wildcard within the first label only. The ssl_verify_fun result therefore decides the outcome only with `{server_name_indication, disable}`, in the server role, on direct calls to `verify_cert_hostname/2`, or on OTP older than 20.0. ## Proof of concept 1. Issue a leaf certificate whose only `subjectAltName` entry is `dNSName` `f*.example.net`, for example with `public_key:pkix_test_data/1`, and decode it with `public_key:pkix_decode_cert(Der, otp)`. 2. Call `ssl_verify_hostname:verify_cert_hostname(Cert, "foo.example.net")`. It returns `{valid, "foo.example.net"}`, as expected. 3. Call `ssl_verify_hostname:verify_cert_hostname(Cert, "f.evil.example.net")`. It returns `{valid, "f.evil.example.net"}` although the hostname spans two labels where the wildcard sits. 4. Call `ssl_verify_hostname:verify_cert_hostname(Cert, "a.b.example.net")` as a control. It returns `{fail, unable_to_match_altnames}`. 5. For the IP-literal variant, issue a leaf with `dNSName` `*.168.1.1` and call `verify_cert_hostname(Cert, "192.168.1.1")`. It returns `{valid, "192.168.1.1"}`. 6. To observe the full TLS path, serve the first certificate from `ssl:listen/2` and connect with `{verify, verify_peer}`, the CA in `cacerts`, `{verify_fun, {fun ssl_verify_hostname:verify_fun/3, [{check_hostname, "f.evil.example.net"}]}}` and `{server_name_indication, disable}`. The handshake succeeds. With `{server_name_indication, "f.evil.example.net"}` instead, OTP 20.0 or later rejects the certificate with `hostname_check_failed` before ssl_verify_fun runs. ## Impact A holder of a certificate for `f*.example.net` from a CA the client trusts can impersonate any host named `f.<label>.example.net` to a client that relies on ssl_verify_fun alone for hostname verification, and read or alter the traffic of that connection. ## Workarounds On Erlang/OTP 21 or later, keep `server_name_indication` set to the target hostname and pass `{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]}` in the `ssl` options. The hostname check in `ssl` then rejects the mismatch before ssl_verify_fun runs, and legitimate wildcard certificates continue to match. hackney and tls_certificate_check already set both options. ## Configurations ssl_verify_fun decides the outcome only where it is the sole hostname check. Erlang/OTP 20.0 and later verify the hostname in `ssl` before the user `verify_fun` is called with `valid_peer`, as long as `server_name_indication` is a hostname or the connection was upgraded from a socket. A deployment is exposed when it sets `{server_name_indication, disable}`, when a TLS server uses `ssl_verify_hostname:verify_fun/3` to verify client certificates, when it calls `ssl_verify_hostname:verify_cert_hostname/2` directly, or when it runs Erlang/OTP older than 20.0. The attacker also needs a certificate with a partial-label wildcard from a CA the client trusts, which the CA/Browser Forum Baseline Requirements do not allow, so the realistic exposure is private PKI.
References: https://github.com/deadtrickster/ssl_verify_fun.erl/security/advisories/GHSA-7gw2-hphm-c5px, https://cna.erlef.org/cves/CVE-2026-117666.html, https://github.com/deadtrickster/ssl_verify_fun.erl/pull/33, https://github.com/deadtrickster/ssl_verify_fun.erl/commit/a49ca56923356c7a3981692b23c42ef32c2990e0, https://github.com/deadtrickster/ssl_verify_fun.erl/commit/ca34031299cd2e0c98794c0da4ba556b94e39aad, https://hex.pm/packages/ssl_verify_fun
Affected packages
Package
Name: ssl_verify_fun
Purl: pkg:hex/ssl_verify_fun
Affected ranges
Type: SEMVER
Events:
