Am I affected?

type your ssl_verify_fun version to check

Description

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.

Technical analysis

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.

Weaknesses & attack patterns

Weakness

CWE-297 · Improper Validation of Certificate with Host Mismatch in catalog → MITRE ↗

Attack patterns

CAPEC-94 · Adversary in the Middle (AiTM) MITRE ↗

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.

Affected — Hex / ssl_verify_fun Hex.pm ↗ Repository ↗

≥ 1.0.1 < 1.1.8 affected
every other version: unaffected
cpe cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*
version type semver
modules · source files · routines
modules ssl_verify_hostname
source files src/ssl_verify_hostname.erl
routines ssl_verify_hostname:try_match_wildcard/4 · ssl_verify_hostname:try_match_hostname/2 · ssl_verify_hostname:verify_cert_hostname/2 · ssl_verify_hostname:verify_fun/3

Affected — GitHub / deadtrickster/ssl_verify_fun.erl Repository ↗

≥ a49ca56 < ca34031 affected
every other version: unaffected
cpe cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*
version type git
modules · source files · routines
modules ssl_verify_hostname
source files src/ssl_verify_hostname.erl
routines ssl_verify_hostname:try_match_wildcard/4 · ssl_verify_hostname:try_match_hostname/2 · ssl_verify_hostname:verify_cert_hostname/2 · ssl_verify_hostname:verify_fun/3

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

Credits

Andrew Moore Finder Reporter
Martin Karrer Finder Remediation developer
deadtrickster Remediation developer
Jonatan Männchen / EEF Coordinator

CVSS breakdown

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
« All CVEs