critical

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/1 and JSON.decode!/1 in Elixir 1.18.0-rc.0 through < 1.18.6, 1.19.x < 1.19.7, and 1.20.x < 1.20.5 passed integer literals of any length directly to :erlang.binary_to_integer/1. Because phx.new Phoenix applications use Elixir’s built-in JSON module inside Plug.Parsers before 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 uncaught SystemLimitError past ~1.29M digits. Commit 7154e8781d caps 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/1 in lib/elixir/lib/uri.ex) converted the port component with String.to_integer/1 without 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 in URI.to_string/1, inspect/1, or request loggers on Erlang/OTP 27 to 29.0 (commit 07d9abc3f3).
  • CVE-2026-97209 (GHSA-96q6-xhwp-fc8q, CVSS 5.4 Medium): In Elixir 1.20.0-rc.4 through < 1.20.5, Mix.Tasks.Deps.Partition.server/4 (mix deps.compile with partitioned compilation) created a predictable /tmp/mix_deps_partition directory via File.mkdir_p/1 (which accepts a pre-existing directory owned by another local user) and also bound its control socket on 0.0.0.0 rather than 127.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 (commit 55eb91220a).

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:

  1. Erlang hackney < 4.9.0 (CVE-2026-115693, CVSS 9.1 Critical, plus four additional CVEs): Since hackney 1.24.0, hackney_ssl:merge_ssl_opts/3 injected {partial_chain, fun hackney_ssl:partial_chain/1} into every verify_peer TLS connection and kept that callback even when the caller pinned a private CA via cacerts or cacertfile in ssl_options. Because Erlang/OTP’s :ssl application honors partial_chain over cacerts, any server presenting a certificate chain rooted in the bundled public certifi store was accepted as trusted for a private-CA-pinned connection (CVE-2026-115693). hackney 4.9.0 (4.9.1 current) 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%5D and trailing-dot IPv4 literals 127.0.0.1. (CVE-2026-115700, CVSS 6.9 Medium), https -> http or cross-port same-host redirect credential leaks (CVE-2026-113440, CVSS 6.0 Medium), and proxy_auth_fun CONNECT CRLF injection (CVE-2026-112880).
  2. Elixir ash < 3.37.2 (CVE-2026-96581 / GHSA-vw7c-67pf-gh96, CVSS 7.6 High): Under strategy :attribute multitenancy (where tenants share a table scoped by an attribute such as org_id), Ash.Actions.Create and Ash.Actions.Create.Bulk performed upserts (upsert? true) on the primary key or an all_tenants?: true identity 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 record id (or matching email/key) overwrote that record, returned all of its attributes to the attacker, and with default upsert_fields rewrote the tenant column (org_id) to transfer ownership of the record into the attacker’s tenant.
  3. 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+, shipped 1.14.0 to 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 a WebSock callback (including a failed auth check in init/1) returned {:stop, ...}, Bandit.WebSocket.Connection sent a Close frame and transitioned to :closing, but handle_frame/3 and handle_info/3 never checked state == :closing. A client that ignored the server’s Close frame could keep sending WebSocket data frames and executing handle_in/2 callbacks 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.Socket read chunk-size lines up to \r\n and discarded everything after ; without rejecting bare \n or \r bytes inside the chunk extension, enabling request smuggling behind proxies that terminate the chunk-size line at a bare \n.
    • HTTP/2 StreamError state rollback (CVE-2026-111103, CVSS 8.2) and seven CVSS 8.7 memory/CPU exhaustion primitives: StreamError mid-chunk rolled back connection stream counters while leaving already-spawned Plug processes running (CVE-2026-111103); inbound DATA frames 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-advertised SETTINGS_HEADER_TABLE_SIZE up to $2^32-1$ grew per-connection HPACK state (CVE-2026-110499); 4,000-digit Content-Length / Host port strings triggered quadratic bignum parsing (CVE-2026-113436); SETTINGS / WINDOW_UPDATE frames caused $O(n^2)$ List.delete/2 walks 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$.

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

EcosystemPackageCVE / AdvisorySeverityAffected versionsFixed versionSummary of flaw
Hex (Erlang / Elixir)hackneyCVE-2026-115693 (EEF-CVE-2026-115693)CVSS 9.1 Critical>= 1.24.0, < 4.9.04.9.0 (4.9.1)hackney_ssl:merge_ssl_opts/3 keeps certifi partial_chain when cacerts / cacertfile is pinned
Hex (Erlang / Elixir)hackneyCVE-2026-97200, CVE-2026-115700, CVE-2026-113440, CVE-2026-112880CVSS 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)ashCVE-2026-96581 (GHSA-vw7c-67pf-gh96)CVSS 7.6 High>= 1.22.0, < 3.37.23.37.2Upsert create actions under :attribute multitenancy overwrite, read, and transfer records across tenants
Hex (Elixir)banditCVE-2026-108525, 108529, 109584, 110499, 111101, 113436, 114213, 115269CVSS 8.7 High (all 8)< 1.14.01.14.0Unenforced 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)banditCVE-2026-111103 (GHSA-2238-pgfc-w82m)CVSS 8.2 High>= 1.9.0, < 1.14.01.14.0StreamError mid-chunk rolls back connection stream count and rapid-reset state while Plugs stay running
Hex (Elixir)banditCVE-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.0WebSocket 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 (commit 452620a92e): SSRF filters in Erlang/Elixir commonly parse a URL with uri_string:parse/1 and test whether the host is an IP literal via inet: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./) caused inet:parse_address/1 in the caller’s allowlist to treat the host as a domain name, while hackney_url:normalize/2 and hackney_happy:parse_address/1 stripped the brackets / trailing dot and connected to 127.0.0.1.
  • CVE-2026-97200 (commit 493e8bf83a): Adds a default max_body cap and streaming decompression so hackney:body/1 and {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:

  1. Policies were bypassed: Ash’s read and update policies do not execute during an upsert create action, and the create policy evaluates against the changeset’s own tenant (changeset.tenant), which is the attacker’s tenant.
  2. Default upsert_fields rewrote the tenant attribute: Unless the action explicitly restricted upsert_fields, the conflict update also overwrote org_id with 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, commit bb407f2277 / 1.14.0): When a WebSock callback returned {:stop, reason, state}, Bandit.WebSocket.Connection invoked terminate/2, sent a WebSocket Close frame, and set connection.state = :closing while waiting for the client’s Close reply. Because handle_frame/3 and handle_info/3 did not pattern-match on state: :open, any client that ignored the server’s Close frame and sent subsequent Text or Binary frames continued invoking handle_in/2 until TCP disconnect or idle timeout.
  • HTTP/2 StreamError connection-state rollback (CVE-2026-111103, commit 3ad49c80c2): In Bandit.HTTP2.Handler.handle_data/3, if a single TCP read contained multiple HTTP/2 frames and a later frame raised StreamError, the rescue block sent RST_STREAM and resumed with the state.connection struct from before the TCP chunk was parsed. All streams spawned by earlier HEADERS frames in that same TCP write continued running their Plug handlers in separate BEAM processes, while max_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 (commit e0c8c0c30a) parsed Content-Length and :authority / Host ports 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 (commit 44bf969cd5) joined tens of thousands of 1-byte HPACK-indexed cookie header references without enforcing a finite max_header_list_size, turning a 49 KB HEADERS block into a 183 MB allocation per stream (CVE-2026-109584).
    • Bandit.HTTP1.Socket (commit c31e9e3c76) now validates chunk extensions against the RFC 9112 grammar and rejects bare \n and \r bytes (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
  1. Upgrade ash, hackney, and bandit:
    • Bump ash to >= 3.37.2 in mix.exs and run mix deps.update ash. If you cannot upgrade immediately on a resource using strategy :attribute, remove id from accept on upsert? true create actions and ensure any upsert_identity has all_tenants?: false (so the conflict target includes the tenant attribute).
    • Bump hackney to >= 4.9.1 (4.9.0+), including where pulled transitively by tesla, httpoison, ex_aws, or oauth2.
    • Bump bandit to >= 1.14.0 across all Phoenix and Plug services.

Timeline

  • 2026-10-25 to 2026-10-26: Maintainers merge fixes and publish hackney 4.9.0 / 4.9.1 (04:44 and 05:04 UTC on 26 October), ash 3.37.2 (05:30 UTC on 26 October), and bandit 1.14.0 (13:12 UTC on 26 October) to Hex.pm.
  • 2026-10-26 (08:17 to 14:16 UTC): The Erlang Ecosystem Foundation CNA and NVD publish CVE-2026-96581 (ash), CVE-2026-115693 / 115700 / 97200 / 113440 / 112880 (hackney), and the 11 bandit CVEs (CVE-2026-108525 through CVE-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