CVE-2026-117666 (GCVE-0-2026-117666)
Vulnerability from cvelistv5 – Published: 2026-10-30 20:22 – Updated: 2026-10-30 20:22
VLAI
EPSS
VEX
Title
ssl_verify_fun: partial-label wildcard certificate matches hostnames across a label boundary
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.
Severity
CWE
- CWE-297 - Improper Validation of Certificate with Host Mismatch
Assigner
References
6 references
Impacted products
2 products
| Vendor | Product | Version | CPE status | |
|---|---|---|---|---|
| deadtrickster | ssl_verify_fun |
Affected:
1.0.1 , < 1.1.8
(semver)
cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:* |
||
| deadtrickster | ssl_verify_fun |
Affected:
a49ca56923356c7a3981692b23c42ef32c2990e0 , < ca34031299cd2e0c98794c0da4ba556b94e39aad
(git)
cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:* |
{
"containers": {
"cna": {
"affected": [
{
"collectionURL": "https://repo.hex.pm",
"cpes": [
"cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"modules": [
"ssl_verify_hostname"
],
"packageName": "ssl_verify_fun",
"packageURL": "pkg:hex/ssl_verify_fun",
"product": "ssl_verify_fun",
"programFiles": [
"src/ssl_verify_hostname.erl"
],
"programRoutines": [
{
"name": "ssl_verify_hostname:try_match_wildcard/4"
},
{
"name": "ssl_verify_hostname:try_match_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_cert_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_fun/3"
}
],
"repo": "https://github.com/deadtrickster/ssl_verify_fun.erl",
"vendor": "deadtrickster",
"versions": [
{
"lessThan": "1.1.8",
"status": "affected",
"version": "1.0.1",
"versionType": "semver"
}
]
},
{
"collectionURL": "https://github.com",
"cpes": [
"cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"modules": [
"ssl_verify_hostname"
],
"packageName": "deadtrickster/ssl_verify_fun.erl",
"packageURL": "pkg:github/deadtrickster/ssl_verify_fun.erl",
"product": "ssl_verify_fun",
"programFiles": [
"src/ssl_verify_hostname.erl"
],
"programRoutines": [
{
"name": "ssl_verify_hostname:try_match_wildcard/4"
},
{
"name": "ssl_verify_hostname:try_match_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_cert_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_fun/3"
}
],
"repo": "https://github.com/deadtrickster/ssl_verify_fun.erl",
"vendor": "deadtrickster",
"versions": [
{
"lessThan": "ca34031299cd2e0c98794c0da4ba556b94e39aad",
"status": "affected",
"version": "a49ca56923356c7a3981692b23c42ef32c2990e0",
"versionType": "git"
}
]
}
],
"configurations": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003essl_verify_fun decides the outcome only where it is the sole hostname check. Erlang/OTP 20.0 and later verify the hostname in \u003ccode\u003essl\u003c/code\u003e before the user \u003ccode\u003everify_fun\u003c/code\u003e is called with \u003ccode\u003evalid_peer\u003c/code\u003e, as long as \u003ccode\u003eserver_name_indication\u003c/code\u003e is a hostname or the connection was upgraded from a socket. A deployment is exposed when it sets \u003ccode\u003e{server_name_indication, disable}\u003c/code\u003e, when a TLS server uses \u003ccode\u003essl_verify_hostname:verify_fun/3\u003c/code\u003e to verify client certificates, when it calls \u003ccode\u003essl_verify_hostname:verify_cert_hostname/2\u003c/code\u003e 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.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "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."
}
],
"value": "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."
}
],
"cpeApplicability": [
{
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*",
"versionEndExcluding": "1.1.8",
"versionStartIncluding": "1.0.1",
"vulnerable": true
}
],
"negate": false,
"operator": "OR"
}
],
"operator": "AND"
}
],
"credits": [
{
"lang": "en",
"type": "finder",
"value": "Andrew Moore"
},
{
"lang": "en",
"type": "finder",
"value": "Martin Karrer"
},
{
"lang": "en",
"type": "reporter",
"value": "Andrew Moore"
},
{
"lang": "en",
"type": "remediation developer",
"value": "Martin Karrer"
},
{
"lang": "en",
"type": "remediation developer",
"value": "deadtrickster"
},
{
"lang": "en",
"type": "coordinator",
"value": "Jonatan M\u00e4nnchen / EEF"
}
],
"dateAssigned": "2026-10-30T10:26:35.000Z",
"descriptions": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eImproper 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. \u003ccode\u003etry_match_wildcard/4\u003c/code\u003e in \u003ccode\u003esrc/ssl_verify_hostname.erl\u003c/code\u003e anchors only the text before and after the \u003ccode\u003e*\u003c/code\u003e of an identifier such as \u003ccode\u003ef*.example.net\u003c/code\u003e, so the wildcard also consumes dots and the certificate is accepted for \u003ccode\u003ef.evil.example.net\u003c/code\u003e. RFC 6125 confines a wildcard to the left-most label. The same matcher lets \u003ccode\u003e*.168.1.1\u003c/code\u003e match the IP literal \u003ccode\u003e192.168.1.1\u003c/code\u003e. Whole-label wildcards are matched correctly.\u003c/p\u003e\n\u003cp\u003eErlang/OTP 20.0 and later check the hostname in \u003ccode\u003essl\u003c/code\u003e before a user \u003ccode\u003everify_fun\u003c/code\u003e sees \u003ccode\u003evalid_peer\u003c/code\u003e, so a client that keeps \u003ccode\u003eserver_name_indication\u003c/code\u003e set rejects the mismatch regardless. Exposed are clients that disable SNI, servers that verify client certificate hostnames with this library, and direct callers of \u003ccode\u003everify_cert_hostname/2\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eThis issue affects ssl_verify_fun: from 1.0.1 before 1.1.8.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "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.\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."
}
],
"value": "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.\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."
}
],
"impacts": [
{
"capecId": "CAPEC-94",
"descriptions": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eA holder of a certificate for \u003ccode\u003ef*.example.net\u003c/code\u003e from a CA the client trusts can impersonate any host named \u003ccode\u003ef.\u0026lt;label\u0026gt;.example.net\u003c/code\u003e to a client that relies on ssl_verify_fun alone for hostname verification, and read or alter the traffic of that connection.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "A holder of a certificate for `f*.example.net` from a CA the client trusts can impersonate any host named `f.\u003clabel\u003e.example.net` to a client that relies on ssl_verify_fun alone for hostname verification, and read or alter the traffic of that connection."
}
],
"value": "A holder of a certificate for f*.example.net from a CA the client trusts can impersonate any host named f.\u003clabel\u003e.example.net to a client that relies on ssl_verify_fun alone for hostname verification, and read or alter the traffic of that connection."
}
]
}
],
"metrics": [
{
"cvssV4_0": {
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"attackComplexity": "HIGH",
"attackRequirements": "PRESENT",
"attackVector": "NETWORK",
"baseScore": 9.1,
"baseSeverity": "CRITICAL",
"privilegesRequired": "NONE",
"providerUrgency": "NOT_DEFINED",
"subAvailabilityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"subIntegrityImpact": "NONE",
"userInteraction": "NONE",
"valueDensity": "NOT_DEFINED",
"vectorString": "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",
"version": "4.0",
"vulnAvailabilityImpact": "NONE",
"vulnConfidentialityImpact": "HIGH",
"vulnIntegrityImpact": "HIGH",
"vulnerabilityResponseEffort": "NOT_DEFINED"
},
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
]
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-297",
"description": "CWE-297 Improper Validation of Certificate with Host Mismatch",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-10-30T20:22:16.466Z",
"orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"shortName": "EEF"
},
"references": [
{
"name": "ssl_verify_fun: partial-label wildcard certificate matches hostnames across a label boundary",
"tags": [
"vendor-advisory",
"related"
],
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/security/advisories/GHSA-7gw2-hphm-c5px"
},
{
"name": "EEF CNA record for CVE-2026-117666",
"tags": [
"related"
],
"url": "https://cna.erlef.org/cves/CVE-2026-117666.html"
},
{
"name": "OSV record EEF-CVE-2026-117666",
"tags": [
"related"
],
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-117666"
},
{
"name": "Do not let partial-label wildcards match across label boundaries (ssl_verify_fun.erl pull request #33)",
"tags": [
"patch"
],
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/pull/33"
},
{
"name": "Introducing commit a49ca56 in deadtrickster/ssl_verify_fun.erl",
"tags": [
"related"
],
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/a49ca56923356c7a3981692b23c42ef32c2990e0"
},
{
"name": "Fix commit ca34031 in deadtrickster/ssl_verify_fun.erl",
"tags": [
"patch"
],
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/ca34031299cd2e0c98794c0da4ba556b94e39aad"
}
],
"source": {
"discovery": "EXTERNAL"
},
"title": "ssl_verify_fun: partial-label wildcard certificate matches hostnames across a label boundary",
"workarounds": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eOn Erlang/OTP 21 or later, keep \u003ccode\u003eserver_name_indication\u003c/code\u003e set to the target hostname and pass \u003ccode\u003e{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]}\u003c/code\u003e in the \u003ccode\u003essl\u003c/code\u003e options. The hostname check in \u003ccode\u003essl\u003c/code\u003e 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.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "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."
}
],
"value": "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."
}
],
"x_proofOfConcept": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003col\u003e\n\u003cli\u003eIssue a leaf certificate whose only \u003ccode\u003esubjectAltName\u003c/code\u003e entry is \u003ccode\u003edNSName\u003c/code\u003e \u003ccode\u003ef*.example.net\u003c/code\u003e, for example with \u003ccode\u003epublic_key:pkix_test_data/1\u003c/code\u003e, and decode it with \u003ccode\u003epublic_key:pkix_decode_cert(Der, otp)\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eCall \u003ccode\u003essl_verify_hostname:verify_cert_hostname(Cert, \"foo.example.net\")\u003c/code\u003e. It returns \u003ccode\u003e{valid, \"foo.example.net\"}\u003c/code\u003e, as expected.\u003c/li\u003e\n\u003cli\u003eCall \u003ccode\u003essl_verify_hostname:verify_cert_hostname(Cert, \"f.evil.example.net\")\u003c/code\u003e. It returns \u003ccode\u003e{valid, \"f.evil.example.net\"}\u003c/code\u003e although the hostname spans two labels where the wildcard sits.\u003c/li\u003e\n\u003cli\u003eCall \u003ccode\u003essl_verify_hostname:verify_cert_hostname(Cert, \"a.b.example.net\")\u003c/code\u003e as a control. It returns \u003ccode\u003e{fail, unable_to_match_altnames}\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eFor the IP-literal variant, issue a leaf with \u003ccode\u003edNSName\u003c/code\u003e \u003ccode\u003e*.168.1.1\u003c/code\u003e and call \u003ccode\u003everify_cert_hostname(Cert, \"192.168.1.1\")\u003c/code\u003e. It returns \u003ccode\u003e{valid, \"192.168.1.1\"}\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eTo observe the full TLS path, serve the first certificate from \u003ccode\u003essl:listen/2\u003c/code\u003e and connect with \u003ccode\u003e{verify, verify_peer}\u003c/code\u003e, the CA in \u003ccode\u003ecacerts\u003c/code\u003e, \u003ccode\u003e{verify_fun, {fun ssl_verify_hostname:verify_fun/3, [{check_hostname, \"f.evil.example.net\"}]}}\u003c/code\u003e and \u003ccode\u003e{server_name_indication, disable}\u003c/code\u003e. The handshake succeeds. With \u003ccode\u003e{server_name_indication, \"f.evil.example.net\"}\u003c/code\u003e instead, OTP 20.0 or later rejects the certificate with \u003ccode\u003ehostname_check_failed\u003c/code\u003e before ssl_verify_fun runs.\u003c/li\u003e\n\u003c/ol\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "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)`.\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."
}
],
"value": "* 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).\n* Call ssl_verify_hostname:verify_cert_hostname(Cert, \"foo.example.net\"). It returns {valid, \"foo.example.net\"}, as expected.\n* 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.\n* Call ssl_verify_hostname:verify_cert_hostname(Cert, \"a.b.example.net\") as a control. It returns {fail, unable_to_match_altnames}.\n* 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\"}.\n* 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."
}
],
"x_technicalAnalysis": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003e\u003cstrong\u003e1. Identifier parsing.\u003c/strong\u003e \u003ccode\u003essl_verify_hostname:try_match_hostname/2\u003c/code\u003e strips trailing dots, tries an exact case-insensitive match, and otherwise calls \u003ccode\u003eparse_and_validate_wildcard_identifier/2\u003c/code\u003e. That function accepts an identifier with one \u003ccode\u003e*\u003c/code\u003e in the left-most label and at least two labels after it, and classifies it as \u003ccode\u003eSingleCharW\u003c/code\u003e when the label is exactly \u003ccode\u003e*\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Whole-label wildcard, correct.\u003c/strong\u003e For \u003ccode\u003e*.example.net\u003c/code\u003e the matcher compares the text after the \u003ccode\u003e*\u003c/code\u003e with the hostname from its first dot onward, so \u003ccode\u003ea.b.example.net\u003c/code\u003e fails because \u003ccode\u003e.b.example.net\u003c/code\u003e differs from \u003ccode\u003e.example.net\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Partial-label wildcard, incorrect.\u003c/strong\u003e For \u003ccode\u003ef*.example.net\u003c/code\u003e the \u003ccode\u003eSingleCharW\u003c/code\u003e branch is skipped. \u003ccode\u003etry_match_wildcard/4\u003c/code\u003e then checks only that the hostname ends with \u003ccode\u003e.example.net\u003c/code\u003e and starts with \u003ccode\u003ef\u003c/code\u003e. Nothing checks the span consumed by the \u003ccode\u003e*\u003c/code\u003e, so \u003ccode\u003ef.evil.example.net\u003c/code\u003e and \u003ccode\u003ef.a.b.example.net\u003c/code\u003e pass. RFC 6125 section 6.4.3 limits a wildcard to the left-most label. The \u003ccode\u003ewildcard_not_in_a_label/2\u003c/code\u003e guard only rejects internationalized \u003ccode\u003exn--\u003c/code\u003e labels and does not help.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. IP literals.\u003c/strong\u003e The reference identifier is treated as a DNS name whatever its shape, so \u003ccode\u003everify_cert_hostname/2\u003c/code\u003e with hostname \u003ccode\u003e192.168.1.1\u003c/code\u003e matches a \u003ccode\u003edNSName\u003c/code\u003e of \u003ccode\u003e*.168.1.1\u003c/code\u003e through the whole-label branch. RFC 6125 does not apply wildcard matching to IP addresses, and OTP\u0027s \u003ccode\u003essl\u003c/code\u003e converts an IP-literal string to an \u003ccode\u003e{ip, ...}\u003c/code\u003e reference that only matches \u003ccode\u003eiPAddress\u003c/code\u003e entries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e5. Interaction with Erlang/OTP.\u003c/strong\u003e When \u003ccode\u003essl_verify_hostname:verify_fun/3\u003c/code\u003e is used as \u003ccode\u003everify_fun\u003c/code\u003e, OTP 20.0 and later call \u003ccode\u003essl_certificate:validate/4\u003c/code\u003e for \u003ccode\u003evalid_peer\u003c/code\u003e before the user fun. In the client role with \u003ccode\u003eserver_name_indication\u003c/code\u003e set to a hostname, or with a peer IP from a socket upgrade, OTP runs \u003ccode\u003epublic_key:pkix_verify_hostname/3\u003c/code\u003e and hands the user fun \u003ccode\u003e{bad_cert, hostname_check_failed}\u003c/code\u003e instead of \u003ccode\u003evalid_peer\u003c/code\u003e, which ssl_verify_fun turns into \u003ccode\u003e{fail, ...}\u003c/code\u003e. OTP\u0027s default match function allows no wildcards, and its \u003ccode\u003ehttps\u003c/code\u003e match function (\u003ccode\u003epublic_key:pkix_verify_hostname_match_fun/1\u003c/code\u003e) 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 \u003ccode\u003e{server_name_indication, disable}\u003c/code\u003e, in the server role, on direct calls to \u003ccode\u003everify_cert_hostname/2\u003c/code\u003e, or on OTP older than 20.0.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "**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\u0027s `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\u0027s 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."
}
],
"value": "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\n2. 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\n3. 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\n4. 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\u0027s ssl converts an IP-literal string to an {ip, ...} reference that only matches iPAddress entries.\n\n5. 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\u0027s 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."
}
]
}
},
"cveMetadata": {
"assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"assignerShortName": "EEF",
"cveId": "CVE-2026-117666",
"datePublished": "2026-10-30T20:22:16.466Z",
"dateReserved": "2026-10-30T10:00:01.681Z",
"dateUpdated": "2026-10-30T20:22:16.466Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"vulnerability-lookup:meta": {
"epss": {
"cve": "CVE-2026-117666",
"date": "2026-10-31",
"epss": "0.0019",
"percentile": "0.08656"
},
"nvd": {
"cve": {
"affected": [
{
"affectedData": [
{
"collectionURL": "https://repo.hex.pm",
"cpes": [
"cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"modules": [
"ssl_verify_hostname"
],
"packageName": "ssl_verify_fun",
"packageURL": "pkg:hex/ssl_verify_fun",
"product": "ssl_verify_fun",
"programFiles": [
"src/ssl_verify_hostname.erl"
],
"programRoutines": [
{
"name": "ssl_verify_hostname:try_match_wildcard/4"
},
{
"name": "ssl_verify_hostname:try_match_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_cert_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_fun/3"
}
],
"repo": "https://github.com/deadtrickster/ssl_verify_fun.erl",
"vendor": "deadtrickster",
"versions": [
{
"lessThan": "1.1.8",
"status": "affected",
"version": "1.0.1",
"versionType": "semver"
}
]
},
{
"collectionURL": "https://github.com",
"cpes": [
"cpe:2.3:a:deadtrickster:ssl_verify_fun:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"modules": [
"ssl_verify_hostname"
],
"packageName": "deadtrickster/ssl_verify_fun.erl",
"packageURL": "pkg:github/deadtrickster/ssl_verify_fun.erl",
"product": "ssl_verify_fun",
"programFiles": [
"src/ssl_verify_hostname.erl"
],
"programRoutines": [
{
"name": "ssl_verify_hostname:try_match_wildcard/4"
},
{
"name": "ssl_verify_hostname:try_match_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_cert_hostname/2"
},
{
"name": "ssl_verify_hostname:verify_fun/3"
}
],
"repo": "https://github.com/deadtrickster/ssl_verify_fun.erl",
"vendor": "deadtrickster",
"versions": [
{
"lessThan": "ca34031299cd2e0c98794c0da4ba556b94e39aad",
"status": "affected",
"version": "a49ca56923356c7a3981692b23c42ef32c2990e0",
"versionType": "git"
}
]
}
],
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db"
}
],
"cveTags": [],
"descriptions": [
{
"lang": "en",
"value": "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.\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."
}
],
"id": "CVE-2026-117666",
"lastModified": "2026-10-30T21:16:51.447",
"metrics": {
"cvssMetricV40": [
{
"cvssData": {
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"attackComplexity": "HIGH",
"attackRequirements": "PRESENT",
"attackVector": "NETWORK",
"availabilityRequirement": "NOT_DEFINED",
"baseScore": 9.1,
"baseSeverity": "CRITICAL",
"confidentialityRequirement": "NOT_DEFINED",
"exploitMaturity": "NOT_DEFINED",
"integrityRequirement": "NOT_DEFINED",
"modifiedAttackComplexity": "NOT_DEFINED",
"modifiedAttackRequirements": "NOT_DEFINED",
"modifiedAttackVector": "NOT_DEFINED",
"modifiedPrivilegesRequired": "NOT_DEFINED",
"modifiedSubAvailabilityImpact": "NOT_DEFINED",
"modifiedSubConfidentialityImpact": "NOT_DEFINED",
"modifiedSubIntegrityImpact": "NOT_DEFINED",
"modifiedUserInteraction": "NOT_DEFINED",
"modifiedVulnAvailabilityImpact": "NOT_DEFINED",
"modifiedVulnConfidentialityImpact": "NOT_DEFINED",
"modifiedVulnIntegrityImpact": "NOT_DEFINED",
"privilegesRequired": "NONE",
"providerUrgency": "NOT_DEFINED",
"subAvailabilityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"subIntegrityImpact": "NONE",
"userInteraction": "NONE",
"valueDensity": "NOT_DEFINED",
"vectorString": "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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"version": "4.0",
"vulnAvailabilityImpact": "NONE",
"vulnConfidentialityImpact": "HIGH",
"vulnIntegrityImpact": "HIGH",
"vulnerabilityResponseEffort": "NOT_DEFINED"
},
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"type": "Secondary"
}
]
},
"published": "2026-10-30T21:16:51.447",
"references": [
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://cna.erlef.org/cves/CVE-2026-117666.html"
},
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/a49ca56923356c7a3981692b23c42ef32c2990e0"
},
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/commit/ca34031299cd2e0c98794c0da4ba556b94e39aad"
},
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/pull/33"
},
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://github.com/deadtrickster/ssl_verify_fun.erl/security/advisories/GHSA-7gw2-hphm-c5px"
},
{
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-117666"
}
],
"sourceIdentifier": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"vulnStatus": "Received",
"weaknesses": [
{
"description": [
{
"lang": "en",
"value": "CWE-297"
}
],
"source": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"type": "Secondary"
}
]
}
}
}
}
Loading…
Loading…
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
Loading…
Loading…
The MITRE ATT&CK techniques below are AI-generated suggestions, inferred from the description of the
vulnerability by the CIRCL/vulnerability-attack-technique-classification-roberta-base
model, served locally by ML-Gateway.
They have not been verified by an analyst and are provided for guidance only.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Loading…
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.