{ "dataType": "CVE_RECORD", "dataVersion": "5.2", "cveMetadata": { "cveId": "CVE-2026-115693", "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db", "state": "PUBLISHED", "assignerShortName": "EEF", "dateReserved": "2026-10-25T10:45:02.327Z", "datePublished": "2026-10-26T13:49:29.374Z", "dateUpdated": "2026-10-26T13:49:29.374Z" }, "containers": { "cna": { "affected": [ { "collectionURL": "https://repo.hex.pm", "cpes": [ "cpe:2.3:a:benoitc:hackney:*:*:*:*:*:*:*:*" ], "defaultStatus": "unaffected", "modules": [ "hackney_ssl" ], "packageName": "hackney", "packageURL": "pkg:hex/hackney", "product": "hackney", "programFiles": [ "src/hackney_ssl.erl" ], "programRoutines": [ { "name": "hackney_ssl:ssl_opts/2" }, { "name": "hackney_ssl:merge_ssl_opts/3" }, { "name": "hackney_ssl:partial_chain/1" } ], "repo": "https://github.com/benoitc/hackney", "vendor": "benoitc", "versions": [ { "lessThan": "4.9.0", "status": "affected", "version": "1.24.0", "versionType": "semver" } ] }, { "collectionURL": "https://github.com", "cpes": [ "cpe:2.3:a:benoitc:hackney:*:*:*:*:*:*:*:*" ], "defaultStatus": "unaffected", "modules": [ "hackney_ssl" ], "packageName": "benoitc/hackney", "packageURL": "pkg:github/benoitc/hackney", "product": "hackney", "programFiles": [ "src/hackney_ssl.erl" ], "programRoutines": [ { "name": "hackney_ssl:ssl_opts/2" }, { "name": "hackney_ssl:merge_ssl_opts/3" }, { "name": "hackney_ssl:partial_chain/1" } ], "repo": "https://github.com/benoitc/hackney", "vendor": "benoitc", "versions": [ { "lessThan": "915eb5737e9c489d8fc22eec8cdae30e9b027be4", "status": "affected", "version": "d8a9afcf5c3849904274e2eba1df56c63d1a1a0c", "versionType": "git" } ] } ], "configurations": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "
The caller passes cacerts or cacertfile in ssl_options to restrict trust to a private CA, without a partial_chain of its own. Connections that use hackney's default certifi trust store are not affected.
Improper Certificate Validation vulnerability in hackney_ssl in benoitc hackney allows an on-path attacker to impersonate a server pinned to a private certificate authority via a certificate chain that ends in a publicly trusted root. hackney adds {partial_chain, fun hackney_ssl:partial_chain/1} to every verify_peer configuration. The callback returns any certificate in the peer chain whose public key matches a root in the bundled certifi store as the trust anchor, and OTP honours that callback over the configured cacerts. hackney_ssl:merge_ssl_opts/3 kept the callback when the caller replaced the trust store with cacerts or cacertfile in ssl_options, so a connection pinned to a private CA still accepted any chain that carried a publicly trusted root.
The hostname check still applies, so the attacker needs a publicly trusted certificate for the pinned hostname.
\nThis issue affects hackney: from 1.24.0 before 4.9.0.
" }, { "base64": false, "type": "text/markdown", "value": "Improper Certificate Validation vulnerability in `hackney_ssl` in benoitc hackney allows an on-path attacker to impersonate a server pinned to a private certificate authority via a certificate chain that ends in a publicly trusted root. hackney adds `{partial_chain, fun hackney_ssl:partial_chain/1}` to every `verify_peer` configuration. The callback returns any certificate in the peer chain whose public key matches a root in the bundled certifi store as the trust anchor, and OTP honours that callback over the configured `cacerts`. `hackney_ssl:merge_ssl_opts/3` kept the callback when the caller replaced the trust store with `cacerts` or `cacertfile` in `ssl_options`, so a connection pinned to a private CA still accepted any chain that carried a publicly trusted root.\n\nThe hostname check still applies, so the attacker needs a publicly trusted certificate for the pinned hostname.\n\nThis issue affects hackney: from 1.24.0 before 4.9.0." } ], "value": "Improper Certificate Validation vulnerability in hackney_ssl in benoitc hackney allows an on-path attacker to impersonate a server pinned to a private certificate authority via a certificate chain that ends in a publicly trusted root. hackney adds {partial_chain, fun hackney_ssl:partial_chain/1} to every verify_peer configuration. The callback returns any certificate in the peer chain whose public key matches a root in the bundled certifi store as the trust anchor, and OTP honours that callback over the configured cacerts. hackney_ssl:merge_ssl_opts/3 kept the callback when the caller replaced the trust store with cacerts or cacertfile in ssl_options, so a connection pinned to a private CA still accepted any chain that carried a publicly trusted root.\n\nThe hostname check still applies, so the attacker needs a publicly trusted certificate for the pinned hostname.\n\nThis issue affects hackney: from 1.24.0 before 4.9.0." } ], "impacts": [ { "capecId": "CAPEC-94", "descriptions": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "An attacker positioned between the client and a server pinned to a private CA, holding a publicly trusted certificate for the pinned hostname, can impersonate the server and read or modify the traffic the client sends and receives.
" }, { "base64": false, "type": "text/markdown", "value": "An attacker positioned between the client and a server pinned to a private CA, holding a publicly trusted certificate for the pinned hostname, can impersonate the server and read or modify the traffic the client sends and receives." } ], "value": "An attacker positioned between the client and a server pinned to a private CA, holding a publicly trusted certificate for the pinned hostname, can impersonate the server and read or modify the traffic the client sends and receives." } ] } ], "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-295", "description": "CWE-295 Improper Certificate Validation", "lang": "en", "type": "CWE" } ] } ], "providerMetadata": { "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db", "shortName": "EEF", "dateUpdated": "2026-10-26T13:49:29.374Z" }, "references": [ { "name": "Drop the certifi partial_chain when the caller pins cacerts or cacertfile (benoitc/hackney#978)", "tags": [ "related" ], "url": "https://github.com/benoitc/hackney/pull/978" }, { "name": "EEF CNA record for CVE-2026-115693", "tags": [ "related" ], "url": "https://cna.erlef.org/cves/CVE-2026-115693.html" }, { "name": "OSV record EEF-CVE-2026-115693", "tags": [ "related" ], "url": "https://osv.dev/vulnerability/EEF-CVE-2026-115693" }, { "name": "hackney 4.9.0 release notes", "tags": [ "vendor-advisory", "related" ], "url": "https://github.com/benoitc/hackney/releases/tag/4.9.0" }, { "name": "Introducing commit d8a9afc in benoitc/hackney", "tags": [ "related" ], "url": "https://github.com/benoitc/hackney/commit/d8a9afcf5c3849904274e2eba1df56c63d1a1a0c" }, { "name": "Fix commit 915eb57 in benoitc/hackney", "tags": [ "patch" ], "url": "https://github.com/benoitc/hackney/commit/915eb5737e9c489d8fc22eec8cdae30e9b027be4" } ], "source": { "discovery": "EXTERNAL" }, "title": "Custom cacerts or cacertfile do not restrict trust to the pinned CA in hackney", "workarounds": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "Pass your own {partial_chain, Fun} in ssl_options alongside cacerts or cacertfile. A caller-supplied partial_chain replaces hackney's in the merge, and a callback that returns unknown_ca makes OTP validate against the configured cacerts only.
1. Default trust options. hackney_ssl:check_hostname_opts/1 builds the verify_peer options for every TLS connection: the certifi cacerts, a hostname verify_fun, and {partial_chain, fun hackney_ssl:partial_chain/1}. The callback walks the peer chain and returns {trusted_ca, Cert} for the first certificate whose public key matches a root in the bundled certifi store, which lets OTP accept chains that end in a cross-signed root.
2. Merge keeps the callback. When the caller passes ssl_options, hackney_ssl:merge_ssl_opts/3 merges them over the defaults with the caller's values winning per key. cacerts or cacertfile replace the trust store, and a cacertfile removes the default cacerts, but partial_chain has no counterpart in the caller's options and stays.
3. Validation. OTP consults partial_chain before the configured trust store. A peer chain that carries any publicly trusted root therefore gets that root as its trust anchor, and the pinned cacerts are never consulted. Only the hostname verify_fun still constrains the attacker.