EEF-CVE-2026-117666

Source
https://cna.erlef.org/osv/EEF-CVE-2026-117666.html
Import Source
https://cna.erlef.org/osv/EEF-CVE-2026-117666.json
JSON Data
https://api.osv.dev/v1/vulns/EEF-CVE-2026-117666
Aliases
  • CVE-2026-117666
  • GHSA-7gw2-hphm-c5px
Published
2026-10-30T20:22:16Z
Modified
2026-10-30T20:41:08Z
Severity
  • 9.1 (Critical) CVSS_V4 - CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N CVSS Calculator
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.

Database specific
{
    "capec_ids":  [
        "CAPEC-94"
    ],
    "cpe_ids":  [
        "cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
    ],
    "cwe_ids":  [
        "CWE-297"
    ]
}
References
Credits
    • Andrew Moore - FINDER
    • Martin Karrer - FINDER
    • Andrew Moore - REPORTER
    • Martin Karrer - REMEDIATION_DEVELOPER
    • deadtrickster - REMEDIATION_DEVELOPER
    • Jonatan Männchen / EEF - COORDINATOR

Affected packages

Hex / ssl_verify_fun

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.*
1.0.8
1.1.0
1.1.1
1.1.2
1.1.3
1.1.4
1.1.5
1.1.6
1.1.7

Database specific

source
"https://cna.erlef.org/osv/EEF-CVE-2026-117666.json"

Git / github.com/deadtrickster/ssl_verify_fun.erl

Affected ranges

Type
GIT
Repo
https://github.com/deadtrickster/ssl_verify_fun.erl
Events

Affected versions

1.*
1.0.1
1.0.3
1.0.4
1.0.5
1.0.6
1.0.8
1.0.9
1.1.0
1.1.1
1.1.2
1.1.3
1.1.4
1.1.5
1.1.6
1.1.7

Database specific

source
"https://cna.erlef.org/osv/EEF-CVE-2026-117666.json"