{"id":"EEF-CVE-2026-96618","summary":"Hex exhausts memory and disk on oversized or compressed responses from an untrusted mirror","details":"## Summary\n\nImproper Handling of Highly Compressed Data (Data Amplification) vulnerability in hexpm hex allows an untrusted package mirror to exhaust the memory or disk space of the machine running `mix deps.get`, `mix deps.update`, `mix hex.package fetch` or `mix hex.docs fetch`.\n\nHex treats a mirror configured with `HEX_MIRROR_URL`, `HEX_MIRROR` or `mirror_url` as untrusted: it doesn't send credentials to it, and it checks registry signatures and package checksums that the mirror can't forge. Several response paths consume memory or disk in proportion to what the mirror sends before any of those checks run.\n\n`Hex.HTTP.request/5` reads the whole response body into memory and decompresses it with `:zlib.gunzip/1` whenever the response's `Content-Encoding` contains `gzip`, with no limit on the compressed or decompressed size. Hex doesn't ask for compressed responses, so the server decides whether this step runs. It applies to registry, package, docs and API responses.\n\nPackage tarballs have no download size limit. Hex 2.5.1 holds the whole tarball in memory and writes it to the cache, and the development branch streams it to a temporary file; the 16 MiB tarball limit is checked only when the file is unpacked. The tarball is then unpacked, into `deps/\u003capp\u003e` in 2.5.1 and into a staging directory on the development branch, before its checksums are compared with the registry.\n\n`mix hex.docs fetch`, and `mix hex.docs offline` when docs are missing, write the docs tarball to disk and extract it with `:mix_hex_erl_tar.extract/2` without the 16 MiB compressed and 128 MiB decompressed limits that `mix_hex_tarball:unpack_docs/3` applies. The signed registry doesn't cover docs tarballs, so nothing checks them.\n\nRegistry responses are also decompressed before their signature is checked, in hex_core code that Hex vendors; that part is tracked in https://github.com/hexpm/hex_core/security/advisories/GHSA-7c4r-cgf9-9jmq.\n\nThis issue affects hex: from 0.11.2 before 2.5.2.\n\n## Details\n\n1. `Hex.HTTP.request/5` calls `:httpc.request/5` with `body_format: :binary`, so the whole body is held in memory, and then calls `:zlib.gunzip/1` when the `content-encoding` header contains `gzip`. Hex doesn't send `Accept-Encoding`.\n2. When `mirror_url` is set, `Hex.Repo` sends registry, tarball and docs requests to the mirror and marks it untrusted, so no credentials are sent to it.\n3. Hex 2.5.1: `Hex.SCM.fetch/3` writes the body returned by `Hex.Repo.get_tarball/3` to the cache. `Hex.SCM.update/1` deletes `deps/\u003capp\u003e`, unpacks the tarball into it with `Hex.Tar.unpack!/2` and compares the inner and outer checksums afterwards. Development branch: `Hex.Repo.get_tarball_to_file/4` streams the tarball to a temporary file without counting bytes; the file is renamed into the cache and unpacked into a staging directory before the checksums are compared.\n4. `Mix.Tasks.Hex.Docs` writes the body returned by `Hex.Repo.get_docs/3` to disk and calls `:mix_hex_erl_tar.extract/2` with `[:compressed, cwd: ...]` and no `max_size`, while `mix_hex_tarball:unpack_docs/3` enforces a 16 MiB compressed and 128 MiB decompressed limit.\n\n## Proof of concept\n\n1. Run a loopback HTTP server that answers with a 65,255-byte gzip stream decompressing to 64 MiB and `Content-Encoding: gzip`. `Hex.HTTP.request(:get, url, %{}, nil)` returns a 67,108,864-byte body; peak RSS was 220 MiB, against 91 MiB for an uncompressed control.\n2. Serve a 17 MiB file as a package tarball. `Hex.Repo.get_tarball/3` returns all of it, and only the following unpack fails with `{:tarball, :too_big}`.\n3. Serve a 131,574-byte docs tarball containing a 129 MiB file and set `HEX_MIRROR_URL` to the server. `mix hex.docs fetch probe 1.0.0` writes the 129 MiB file and reports success.\n\n## Impact\n\nA mirror operator, or an attacker who controls the mirror's responses, can make Hex allocate memory or write data far larger than what it sent: in tests against Hex main 851b938, a 65 KB gzip-encoded response decompressed to 64 MiB in memory, a 17 MiB tarball was downloaded in full despite the 16 MiB limit, and a 131 KB docs tarball wrote 129 MiB to disk. Larger responses stop the Erlang VM running Mix or fill the disk, failing builds and CI jobs, and a full disk affects other processes on the machine. No package code has to run, and the mirror doesn't need the repository's signing key.\n\n## Workarounds\n\nFetch from repo.hex.pm or a mirror you operate. Running Mix under operating system memory and disk limits, such as container limits, confines the effect to that container.\n\n## Configurations\n\nAffects users who fetch packages or docs through a mirror set with `HEX_MIRROR_URL`, `HEX_MIRROR` or `mirror_url`. Fetches from repo.hex.pm over HTTPS aren't affected unless TLS verification is broken. A trusted mirror (`HEX_TRUSTED_MIRROR_URL`) or a self-hosted repository can do the same, but Hex already trusts those with credentials.\n\n## Solutions\n\nUpgrade Hex to 2.5.2 or later, for example with `mix local.hex`. Hex then stops downloading a registry file, package, docs tarball or other file from a repository or mirror once it's larger than 16 MiB, including after decompressing a gzip-encoded response, and checks packages against the registry checksum before writing them to the package cache or unpacking a cached copy.","aliases":["CVE-2026-96618","GHSA-gf42-pg2f-63jp"],"modified":"2026-10-12T15:41:14.855259329Z","published":"2026-10-12T15:29:19.117Z","database_specific":{"capec_ids":["CAPEC-130"],"cpe_ids":["cpe:2.3:a:hexpm:hex:*:*:*:*:*:*:*:*"],"cwe_ids":["CWE-409","CWE-770"]},"references":[{"type":"ADVISORY","url":"https://github.com/hexpm/hex/security/advisories/GHSA-gf42-pg2f-63jp"},{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-96618.html"},{"type":"WEB","url":"https://github.com/hexpm/hex/commit/51ea632424fb6ea8765148f96dc25dfeb526b1f8"},{"type":"FIX","url":"https://github.com/hexpm/hex/commit/bb5189eade682c5fc3f98d9725973a7beb4e1deb"},{"type":"FIX","url":"https://github.com/hexpm/hex/commit/08956100a8283ba500a123e5539ac64d5d7e91d8"}],"affected":[{"ranges":[{"type":"GIT","repo":"https://github.com/hexpm/hex","events":[{"introduced":"51ea632424fb6ea8765148f96dc25dfeb526b1f8"},{"fixed":"bb5189eade682c5fc3f98d9725973a7beb4e1deb"},{"fixed":"08956100a8283ba500a123e5539ac64d5d7e91d8"}]}],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-96618.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N"}],"credits":[{"name":"Peter Ullrich","type":"FINDER"},{"name":"Eric Meadows-Jönsson","type":"REMEDIATION_DEVELOPER"},{"name":"Jonatan Männchen / EEF","type":"ANALYST"}]}