EEF-CVE-2026-117666

    Dashboard / Vulnerabilities / EEF-CVE-2026-117666

    EEF-CVE-2026-117666

    Published: 30 Oct 2026Last Modified: 30 Oct 2026

    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.

    Affected packages

    Package

    Name: ssl_verify_fun

    Purl: pkg:hex/ssl_verify_fun

    Affected ranges

    Type: SEMVER

    Events:

    Introduced- 1.0.1
    Fixed -1.1.8

    Affected versions

    1.0.8

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High
    EEF-CVE-2026-117666 | CVE-DB