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.
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.
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).ssl_verify_hostname:verify_cert_hostname(Cert, "foo.example.net"). It returns {valid, "foo.example.net"}, as expected.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.ssl_verify_hostname:verify_cert_hostname(Cert, "a.b.example.net") as a control. It returns {fail, unable_to_match_altnames}.dNSName *.168.1.1 and call verify_cert_hostname(Cert, "192.168.1.1"). It returns {valid, "192.168.1.1"}.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.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.
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.
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.
{
"capec_ids": [
"CAPEC-94"
],
"cpe_ids": [
"cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
],
"cwe_ids": [
"CWE-297"
]
}