eef-cve-2026-117666

CVSS 9.1 osv_hex
Description

## 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.

Timeline
Published
unknown
Last Modified
unknown
CVSS Details

CVSS details not available.

Affected Products

No product information available.

Weaknesses (CWE)
References
Linked Vulnerabilities

No linked vulnerabilities found.

{
  "affected": [
    {
      "database_specific": {
        "source": "https://cna.erlef.org/osv/EEF-CVE-2026-117666.json"
      },
      "package": {
        "ecosystem": "Hex",
        "name": "ssl_verify_fun",
        "purl": "pkg:hex/ssl_verify_fun"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.1"
            },
            {
              "fixed": "1.1.8"
            }
          ],
          "type": "SEMVER"
        }
      ],
      "versions": [
        "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"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "a49ca56923356c7a3981692b23c42ef32c2990e0"
            },
            {
              "fixed": "ca34031299cd2e0c98794c0da4ba556b94e39aad"
            }
          ],
          "repo": "https://github.com/deadtrickster/ssl_verify_fun.erl",
          "type": "GIT"
        }
      ],
      "versions": [
        "1.1.7",
        "1.1.6",
        "1.1.5",
        "1.1.4",
        "1.1.3",
        "1.1.2",
        "1.1.1",
        "1.1.0",
        "1.0.9",
        "1.0.8",
        "1.0.6",
        "1.0.5",
        "1.0.4",
        "1.0.3",
        "1.0.1"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-117666",
    "GHSA-7gw2-hphm-c5px"
  ],
  "credits": [
    {
      "name": "Andrew Moore",
      "type": "FINDER"
    },
    {
      "name": "Martin Karrer",
      "type": "FINDER"
    },
    {
      "name": "Andrew Moore",
      "type": "REPORTER"
    },
    {
      "name": "Martin Karrer",
      "type": "REMEDIATION_DEVELOPER"
    },
    {
      "name": "deadtrickster",
      "type": "REMEDIATION_DEVELOPER"
    },
    {
      "name": "Jonatan Männchen / EEF",
      "type": "COORDINATOR"
    }
  ],
  "database_specific": {
    "capec_ids": [
      "CAPEC-94"
    ],
    "cpe_ids": [
      "cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
    ],
    "cwe_ids": [
      "CWE-297"
    ]
  },
  "details": "## Summary\n\nImproper 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.\n\nErlang/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`.\n\nThis issue affects ssl_verify_fun: from 1.0.1 before 1.1.8.\n\n## Details\n\n**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 `*`.\n\n**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`.\n\n**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.\n\n**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.\n\n**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.\n\n## Proof of concept\n\n1. 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)`.\n2. Call `ssl_verify_hostname:verify_cert_hostname(Cert, \"foo.example.net\")`. It returns `{valid, \"foo.example.net\"}`, as expected.\n3. 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.\n4. Call `ssl_verify_hostname:verify_cert_hostname(Cert, \"a.b.example.net\")` as a control. It returns `{fail, unable_to_match_altnames}`.\n5. 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\"}`.\n6. 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.\n\n## Impact\n\nA 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.\n\n## Workarounds\n\nOn 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.\n\n## Configurations\n\nssl_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.",
  "id": "EEF-CVE-2026-117666",
  "modified": "2026-10-30T20:41:08.976521139Z",
  "published": "2026-10-30T20:22:16.466Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://github.com/deadtrickster/ssl_verify_fun.erl/security/advisories/GHSA-7gw2-hphm-c5px"
    },
    {
      "type": "WEB",
      "url": "https://cna.erlef.org/cves/CVE-2026-117666.html"
    },
    {
      "type": "FIX",
      "url": "https://github.com/deadtrickster/ssl_verify_fun.erl/pull/33"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/a49ca56923356c7a3981692b23c42ef32c2990e0"
    },
    {
      "type": "FIX",
      "url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/ca34031299cd2e0c98794c0da4ba556b94e39aad"
    },
    {
      "type": "PACKAGE",
      "url": "https://hex.pm/packages/ssl_verify_fun"
    }
  ],
  "schema_version": "1.9.2",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "ssl_verify_fun: partial-label wildcard certificate matches hostnames across a label boundary"
}
View JSON API Download JSON