CVE
CVE-2026-115693, CVE-2026-96581, CVE-2026-108525, CVE-2026-108529, CVE-2026-109584, CVE-2026-110314, CVE-2026-110499, CVE-2026-111101, CVE-2026-111103, CVE-2026-113436, CVE-2026-114213, CVE-2026-114216, CVE-2026-115269, CVE-2026-97200, CVE-2026-115700, CVE-2026-113440, CVE-2026-112880, CVE-2026-97209, CVE-2026-115274, CVE-2026-109821
CWE
CWE-295, CWE-639, CWE-400, CWE-407, CWE-444, CWE-918, CWE-377
Affected Surface
- Elixir standard library (`elixir`) and `mix` versions `< 1.18.6`, `1.19.x < 1.19.7`, and `1.20.x < 1.20.5` (`CVE-2026-115274` / `GHSA-rp5q-xqhq-926h` in `JSON.decode/1`, `CVE-2026-109821` / `GHSA-327h-97cv-59xg` in `URI.parse/1`, and `CVE-2026-97209` / `GHSA-96q6-xhwp-fc8q` in `mix deps.partition` for `1.20.0-rc.4` to `< 1.20.5`)
- Hex.pm `ash` (`github.com/ash-project/ash`) versions `>= 1.22.0, < 3.37.2` using `strategy :attribute` multitenancy with `upsert? true` create actions (`CVE-2026-96581` / `GHSA-vw7c-67pf-gh96`)
- Hex.pm `hackney` (`github.com/benoitc/hackney`) versions `< 4.9.0` (`>= 1.24.0, < 4.9.0` for `CVE-2026-115693`; `>= 2.0.0-beta.1, < 4.9.0` for `CVE-2026-97200`, `CVE-2026-113440`, and `CVE-2026-112880`; `>= 4.0.1, < 4.9.0` for `CVE-2026-115700`)
- Hex.pm `bandit` (`github.com/mtrudel/bandit`) versions `< 1.14.0` serving Phoenix, Plug, or WebSock applications over HTTP/1.1, HTTP/2, or WebSockets (`CVE-2026-108525`, `CVE-2026-108529`, `CVE-2026-109584`, `CVE-2026-110314`, `CVE-2026-110499`, `CVE-2026-111101`, `CVE-2026-111103`, `CVE-2026-113436`, `CVE-2026-114213`, `CVE-2026-114216`, `CVE-2026-115269`)
Update, 30 October 2026: At 09:17 UTC on 30 October, the Erlang Ecosystem Foundation (EEF) CNA and NVD published three additional CVEs in the Elixir standard library and mix build tool, patched in Elixir 1.20.5, 1.19.7, and 1.18.6:
CVE-2026-115274(GHSA-rp5q-xqhq-926h, CVSS 6.9 Medium):JSON.decode/1andJSON.decode!/1in Elixir1.18.0-rc.0through< 1.18.6,1.19.x < 1.19.7, and1.20.x < 1.20.5passed integer literals of any length directly to:erlang.binary_to_integer/1. Becausephx.newPhoenix applications use Elixir’s built-inJSONmodule insidePlug.Parsersbefore routing or authentication, a single unauthenticated request body containing a 1,000,000-digit integer consumed $100\times$ to $300\times$ the CPU of a string of the same byte length (~1 second of scheduler time per few megabytes) or raised an uncaughtSystemLimitErrorpast ~1.29M digits. Commit7154e8781dcaps JSON integer literals at 310 digits ({:error, {:integer_too_large, digits}}).CVE-2026-109821(GHSA-327h-97cv-59xg, CVSS 6.3 Medium):URI.parse/1(split_authority/1inlib/elixir/lib/uri.ex) converted the port component withString.to_integer/1without checking length or the 65,535 TCP port bound. Supplying a URL with a 400,000- to 1,000,000-digit port produced a giant bignum that took 2.6 to 18 seconds of CPU to format back to text inURI.to_string/1,inspect/1, or request loggers on Erlang/OTP 27 to 29.0 (commit07d9abc3f3).CVE-2026-97209(GHSA-96q6-xhwp-fc8q, CVSS 5.4 Medium): In Elixir1.20.0-rc.4through< 1.20.5,Mix.Tasks.Deps.Partition.server/4(mix deps.compilewith partitioned compilation) created a predictable/tmp/mix_deps_partitiondirectory viaFile.mkdir_p/1(which accepts a pre-existing directory owned by another local user) and also bound its control socket on0.0.0.0rather than127.0.0.1, allowing another local user on a shared build host to swap the serialized partition config file and execute arbitrary code as the building user (commit55eb91220a).
TL;DR
On 26 October 2026, the Erlang Ecosystem Foundation (EEF) CNA and package maintainers published 17 CVEs across three widely deployed BEAM (Elixir and Erlang) dependencies on Hex.pm:
- Erlang
hackney< 4.9.0(CVE-2026-115693, CVSS 9.1 Critical, plus four additional CVEs): Sincehackney 1.24.0,hackney_ssl:merge_ssl_opts/3injected{partial_chain, fun hackney_ssl:partial_chain/1}into everyverify_peerTLS connection and kept that callback even when the caller pinned a private CA viacacertsorcacertfileinssl_options. Because Erlang/OTP’s:sslapplication honorspartial_chainovercacerts, any server presenting a certificate chain rooted in the bundled publiccertifistore was accepted as trusted for a private-CA-pinned connection (CVE-2026-115693).hackney 4.9.0(4.9.1current) also fixes unbounded response body buffering and gzip bomb inflation (CVE-2026-97200, CVSS 8.2 High), SSRF allowlist bypasses via percent-encoded bracketed IPv6 literals%5B%3A%3Affff%3A127%2E0%2E0%2E1%5Dand trailing-dot IPv4 literals127.0.0.1.(CVE-2026-115700, CVSS 6.9 Medium),https -> httpor cross-port same-host redirect credential leaks (CVE-2026-113440, CVSS 6.0 Medium), andproxy_auth_funCONNECT CRLF injection (CVE-2026-112880). - Elixir
ash< 3.37.2(CVE-2026-96581/GHSA-vw7c-67pf-gh96, CVSS 7.6 High): Understrategy :attributemultitenancy (where tenants share a table scoped by an attribute such asorg_id),Ash.Actions.CreateandAsh.Actions.Create.Bulkperformed upserts (upsert? true) on the primary key or anall_tenants?: trueidentity without filtering the conflicting row by the caller’s tenant. Because read and update policies do not run on upserts and create policies see the caller’s own tenant, supplying another tenant’s recordid(or matching email/key) overwrote that record, returned all of its attributes to the attacker, and with defaultupsert_fieldsrewrote the tenant column (org_id) to transfer ownership of the record into the attacker’s tenant. - Elixir
bandit< 1.14.0(11 CVEs, seven rated CVSS 8.7 High and one 8.2 High): Bandit, the default HTTP/1.1, HTTP/2, and WebSocket server for Phoenix 1.7+, shipped1.14.0to fix nine DoS, resource-limit bypass, post-close WebSocket execution, and HTTP/1.1 request-smuggling flaws:- Post-close WebSocket handler execution (
CVE-2026-110314/GHSA-2v5v-8f9v-xh85): When aWebSockcallback (including a failed auth check ininit/1) returned{:stop, ...},Bandit.WebSocket.Connectionsent aCloseframe and transitioned to:closing, buthandle_frame/3andhandle_info/3never checkedstate == :closing. A client that ignored the server’sCloseframe could keep sending WebSocket data frames and executinghandle_in/2callbacks using the state from the stop tuple. - HTTP/1.1 bare-LF chunk-extension request smuggling (
CVE-2026-114216/GHSA-w5wc-g2hc-g7m2):Bandit.HTTP1.Socketread chunk-size lines up to\r\nand discarded everything after;without rejecting bare\nor\rbytes inside the chunk extension, enabling request smuggling behind proxies that terminate the chunk-size line at a bare\n. - HTTP/2
StreamErrorstate rollback (CVE-2026-111103, CVSS 8.2) and seven CVSS 8.7 memory/CPU exhaustion primitives:StreamErrormid-chunk rolled back connection stream counters while leaving already-spawned Plug processes running (CVE-2026-111103); inboundDATAframes ignored the 65,535-byte receive window (CVE-2026-108525); 49 KB HPACK cookie-crumb blocks expanded to 183 MB binaries (CVE-2026-109584); client-advertisedSETTINGS_HEADER_TABLE_SIZEup to $2^32-1$ grew per-connection HPACK state (CVE-2026-110499); 4,000-digitContent-Length/Hostport strings triggered quadratic bignum parsing (CVE-2026-113436);SETTINGS/WINDOW_UPDATEframes caused $O(n^2)$List.delete/2walks over blocked streams (CVE-2026-108529) and reset body read timeouts indefinitely (CVE-2026-115269); and 1-byte HTTP/1.1 chunks (CVE-2026-111101) or 1-byte WebSocket continuation frames (CVE-2026-114213) amplified BEAM list/binary overhead by $10\times$ to $150\times$.
- Post-close WebSocket handler execution (
What to do now: Upgrade ash to >= 3.37.2, hackney to >= 4.9.1 (4.9.0+), and bandit to >= 1.14.0 (mix deps.update ash hackney bandit).
Affected packages
| Ecosystem | Package | CVE / Advisory | Severity | Affected versions | Fixed version | Summary of flaw |
|---|---|---|---|---|---|---|
| Hex (Erlang / Elixir) | hackney | CVE-2026-115693 (EEF-CVE-2026-115693) | CVSS 9.1 Critical | >= 1.24.0, < 4.9.0 | 4.9.0 (4.9.1) | hackney_ssl:merge_ssl_opts/3 keeps certifi partial_chain when cacerts / cacertfile is pinned |
| Hex (Erlang / Elixir) | hackney | CVE-2026-97200, CVE-2026-115700, CVE-2026-113440, CVE-2026-112880 | CVSS 8.2 High to 2.1 Low | < 4.9.0 (115700: >= 4.0.1, < 4.9.0) | 4.9.0 (4.9.1) | Unbounded body/gzip allocation (97200); %5B...%5D IPv6 & trailing-dot IP SSRF bypass (115700); scheme/port redirect auth leak (113440); proxy CONNECT CRLF (112880) |
| Hex (Elixir) | ash | CVE-2026-96581 (GHSA-vw7c-67pf-gh96) | CVSS 7.6 High | >= 1.22.0, < 3.37.2 | 3.37.2 | Upsert create actions under :attribute multitenancy overwrite, read, and transfer records across tenants |
| Hex (Elixir) | bandit | CVE-2026-108525, 108529, 109584, 110499, 111101, 113436, 114213, 115269 | CVSS 8.7 High (all 8) | < 1.14.0 | 1.14.0 | Unenforced HTTP/2 recv window, $O(n^2)$ do_pending_sends, HPACK cookie bomb, uncapped HPACK encoder table, quadratic parse_integer, 1-byte chunk/WS frame amplification, and timeout resets |
| Hex (Elixir) | bandit | CVE-2026-111103 (GHSA-2238-pgfc-w82m) | CVSS 8.2 High | >= 1.9.0, < 1.14.0 | 1.14.0 | StreamError mid-chunk rolls back connection stream count and rapid-reset state while Plugs stay running |
| Hex (Elixir) | bandit | CVE-2026-110314 (GHSA-2v5v-8f9v-xh85), CVE-2026-114216 (GHSA-w5wc-g2hc-g7m2) | CVSS 6.3 Medium (both) | >= 0.5.1, < 1.14.0 (114216: >= 1.12.4, < 1.14.0) | 1.14.0 | WebSocket frames still dispatched to handle_in/2 in :closing state (110314); bare LF in chunk extensions enables HTTP/1.1 request smuggling (114216) |
Technical analysis
1. CVE-2026-115693 and CVE-2026-115700: hackney pinned-CA bypass and IP-literal SSRF bypass
In Erlang/OTP’s :ssl module, when a {partial_chain, Fun} option is present, OTP passes the peer’s certificate chain to Fun and treats any certificate returned as {trusted_ca, Cert} as the trust anchor, taking precedence over the cacerts list.
To work around cross-signed intermediates against the bundled certifi CA store, hackney_ssl:check_hostname_opts/1 added {partial_chain, fun hackney_ssl:partial_chain/1} to DefaultOpts, where partial_chain/1 trusted any certificate whose public key matched certifi:cacerts(). When a caller passed custom ssl_options pinning an internal or mutual-TLS CA ([{ssl_options, [{cacerts, [PinnedRootDer]}]}] or {cacertfile, "/etc/ssl/internal-ca.pem"}), hackney_ssl:merge_ssl_opts/3 replaced cacerts but left hackney’s partial_chain callback in the merged options. Any attacker holding a publicly trusted certificate for the target hostname (for example from Let’s Encrypt or a compromised public CA) passed TLS verification against a client that explicitly pinned a private CA.
Commit 915eb5737e (hackney 4.9.0) strips the default partial_chain whenever the caller overrides cacerts or cacertfile:
%% src/hackney_ssl.erl (commit 915eb5737e)
CustomCA = lists:keymember(cacerts, 1, OverrideOpts)
orelse lists:keymember(cacertfile, 1, OverrideOpts),
case CustomCA andalso not lists:keymember(partial_chain, 1, OverrideOpts) of
true ->
lists:keydelete(partial_chain, 1, MergedOpts1);
false ->
MergedOpts1
end.
In the same 4.9.0 release:
CVE-2026-115700(commit452620a92e): SSRF filters in Erlang/Elixir commonly parse a URL withuri_string:parse/1and test whether the host is an IP literal viainet:parse_address/1. Passing a percent-encoded bracketed IPv6 literal (http://%5B%3A%3Affff%3A127%2E0%2E0%2E1%5D/) or an IPv4 literal with a trailing dot (http://127.0.0.1./) causedinet:parse_address/1in the caller’s allowlist to treat the host as a domain name, whilehackney_url:normalize/2andhackney_happy:parse_address/1stripped the brackets / trailing dot and connected to127.0.0.1.CVE-2026-97200(commit493e8bf83a): Adds a defaultmax_bodycap and streaming decompression sohackney:body/1and{auto_decompress, true}(zlib:gunzip/1) cannot buffer unbounded response data in memory.
2. CVE-2026-96581: cross-tenant record takeover via Ash upsert? true actions
In Elixir’s Ash Framework, resources using multitenancy do strategy :attribute; attribute :org_id end store all tenants’ rows in a single table and scope queries by org_id.
When a create action was configured with upsert? true (or a caller passed upsert?: true) and conflicted on either the primary key (id accepted as an input) or an identity marked all_tenants?: true (such as a globally unique :email), Ash.Actions.Create and Ash.Actions.Create.Bulk generated an INSERT ... ON CONFLICT DO UPDATE statement without adding a WHERE org_id = ^tenant filter to the conflict update clause.
Two things made this immediately exploitable across tenants:
- Policies were bypassed: Ash’s
readandupdatepolicies do not execute during anupsertcreate action, and thecreatepolicy evaluates against the changeset’s own tenant (changeset.tenant), which is the attacker’s tenant. - Default
upsert_fieldsrewrote the tenant attribute: Unless the action explicitly restrictedupsert_fields, the conflict update also overwroteorg_idwith the caller’s tenant, moving the victim tenant’s record into the attacker’s tenant and returning all of its fields in the response.
Commit c9f0cdb31e (ash 3.37.2) attaches Helpers.upsert_tenant_filter(changeset) to the changeset filter on both single and bulk upserts:
# lib/ash/actions/helpers.ex (ash 3.37.2, commit c9f0cdb31e)
def upsert_tenant_filter(changeset) do
if changeset.tenant &&
Ash.Resource.Info.multitenancy_strategy(changeset.resource) == :attribute &&
get_multitenancy_from_context(changeset) not in [:bypass, :bypass_all] do
attribute = Ash.Resource.Info.multitenancy_attribute(changeset.resource)
{m, f, a} = Ash.Resource.Info.multitenancy_parse_attribute(changeset.resource)
[{attribute, apply(m, f, [changeset.to_tenant | a])}]
end
end
3. Bandit 1.14.0: WebSocket post-close execution (CVE-2026-110314), HTTP/2 StreamError rollback (CVE-2026-111103), and HPACK/flow-control DoS
Bandit 1.14.0 patches 11 CVEs across lib/bandit/http1/, lib/bandit/http2/, lib/bandit/websocket/, and lib/bandit/headers.ex:
- WebSocket frames processed after
{:stop, ...}(CVE-2026-110314, commitbb407f2277/1.14.0): When aWebSockcallback returned{:stop, reason, state},Bandit.WebSocket.Connectioninvokedterminate/2, sent a WebSocketCloseframe, and setconnection.state = :closingwhile waiting for the client’sClosereply. Becausehandle_frame/3andhandle_info/3did not pattern-match onstate: :open, any client that ignored the server’sCloseframe and sent subsequentTextorBinaryframes continued invokinghandle_in/2until TCP disconnect or idle timeout. - HTTP/2
StreamErrorconnection-state rollback (CVE-2026-111103, commit3ad49c80c2): InBandit.HTTP2.Handler.handle_data/3, if a single TCP read contained multiple HTTP/2 frames and a later frame raisedStreamError, therescueblock sentRST_STREAMand resumed with thestate.connectionstruct from before the TCP chunk was parsed. All streams spawned by earlierHEADERSframes in that same TCP write continued running their Plug handlers in separate BEAM processes, whilemax_concurrent_streams,max_requests, and the rapid-reset limiter rolled back to their pre-chunk counts. - HTTP/2 HPACK, flow-control, and bignum DoS (
CVE-2026-108525,108529,109584,110499,113436,115269) and HTTP/1.1 / WebSocket frame amplification (CVE-2026-111101,114213,114216):Bandit.Headers.parse_integer/2(commite0c8c0c30a) parsedContent-Lengthand:authority/Hostports digit-by-digit into arbitrary-precision BEAM bignums; a 4,000-digit value stored in the HTTP/2 HPACK dynamic table and referenced by 1-byte indexed headers consumed 27.9 seconds of CPU from 124 KB of traffic (CVE-2026-113436).combine_cookie_crumbs/1(commit44bf969cd5) joined tens of thousands of 1-byte HPACK-indexedcookieheader references without enforcing a finitemax_header_list_size, turning a 49 KBHEADERSblock into a 183 MB allocation per stream (CVE-2026-109584).Bandit.HTTP1.Socket(commitc31e9e3c76) now validates chunk extensions against the RFC 9112 grammar and rejects bare\nand\rbytes (CVE-2026-114216).
Detection and remediation
# 1. Check Elixir mix.lock and Erlang rebar.lock files for vulnerable ash, hackney, and bandit versions
rg -n '"(ash|hackney|bandit)"' mix.lock rebar.lock 2>/dev/null
# 2. Search Ash resources for attribute multitenancy combined with upsert create actions
rg -n -C 5 'strategy\s*\(?:\s*attribute' lib/ 2>/dev/null
rg -n -C 5 'upsert\?\s+true|upsert\?:\s*true' lib/ 2>/dev/null
# 3. Search Erlang / Elixir code for hackney ssl_options pinning cacerts or cacertfile
rg -n 'cacerts|cacertfile' lib/ src/ config/ 2>/dev/null
- Upgrade
ash,hackney, andbandit:- Bump
ashto>= 3.37.2inmix.exsand runmix deps.update ash. If you cannot upgrade immediately on a resource usingstrategy :attribute, removeidfromacceptonupsert? truecreate actions and ensure anyupsert_identityhasall_tenants?: false(so the conflict target includes the tenant attribute). - Bump
hackneyto>= 4.9.1(4.9.0+), including where pulled transitively bytesla,httpoison,ex_aws, oroauth2. - Bump
banditto>= 1.14.0across all Phoenix and Plug services.
- Bump
Timeline
- 2026-10-25 to 2026-10-26: Maintainers merge fixes and publish
hackney 4.9.0/4.9.1(04:44and05:04 UTCon 26 October),ash 3.37.2(05:30 UTCon 26 October), andbandit 1.14.0(13:12 UTCon 26 October) to Hex.pm. - 2026-10-26 (
08:17to14:16 UTC): The Erlang Ecosystem Foundation CNA and NVD publishCVE-2026-96581(ash),CVE-2026-115693/115700/97200/113440/112880(hackney), and the 11banditCVEs (CVE-2026-108525throughCVE-2026-115269).
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Use AI-native static analysis to find similar application vulnerabilities and generate review-ready fixes.
References
- Ash advisory GHSA-vw7c-67pf-gh96 and commit c9f0cdb31e (CVE-2026-96581: attribute multitenancy upsert takeover)
- benoitc/hackney 4.9.0 release and commit 915eb5737e (CVE-2026-115693, CVE-2026-115700, CVE-2026-97200, CVE-2026-113440, CVE-2026-112880)
- Bandit advisories GHSA-c3g7-2pg5-qvwh, GHSA-7gfj-7gvq-3xqj, GHSA-2238-pgfc-w82m, GHSA-2v5v-8f9v-xh85, and GHSA-w5wc-g2hc-g7m2 (v1.14.0)
- EEF CNA: CVE-2026-115693, CVE-2026-96581, and CVE-2026-108525 advisories
- Elixir advisories GHSA-96q6-xhwp-fc8q (CVE-2026-97209), GHSA-rp5q-xqhq-926h (CVE-2026-115274), and GHSA-327h-97cv-59xg (CVE-2026-109821)