{ "dataType": "CVE_RECORD", "dataVersion": "5.2", "cveMetadata": { "cveId": "CVE-2026-96612", "assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db", "state": "PUBLISHED", "assignerShortName": "EEF", "dateReserved": "2026-10-01T09:00:02.103Z", "datePublished": "2026-10-12T15:39:49.862Z", "dateUpdated": "2026-10-12T15:39:49.862Z" }, "containers": { "cna": { "affected": [ { "cpes": [ "cpe:2.3:a:hexpm:hex:*:*:*:*:*:*:*:*" ], "defaultStatus": "unaffected", "modules": [ "'Elixir.Hex.RemoteConverger'", "'Elixir.Hex.Mix'", "'Elixir.Hex.SCM'" ], "packageName": "hex", "packageURL": "pkg:otp/hex?repository_url=https:%2F%2Fgithub.com%2Fhexpm%2Fhex&vcs_url=repository_url=git%2Bhttps:%2F%2Fgithub.com%2Fhexpm%2Fhex.git", "product": "hex", "programFiles": [ "lib/hex/remote_converger.ex", "lib/hex/mix.ex", "lib/hex/scm.ex" ], "programRoutines": [ { "name": "'Elixir.Hex.RemoteConverger':deps/2" }, { "name": "'Elixir.Hex.RemoteConverger':converge/2" }, { "name": "'Elixir.Hex.Mix':to_lock/1" }, { "name": "'Elixir.Hex.SCM':update/1" }, { "name": "'Elixir.Hex.SCM':checkout/1" } ], "repo": "https://github.com/hexpm/hex", "vendor": "hexpm", "versions": [ { "lessThan": "2.5.2", "status": "affected", "version": "0.6.0", "versionType": "semver" } ] }, { "collectionURL": "https://github.com", "cpes": [ "cpe:2.3:a:hexpm:hex:*:*:*:*:*:*:*:*" ], "defaultStatus": "unaffected", "modules": [ "'Elixir.Hex.RemoteConverger'", "'Elixir.Hex.Mix'", "'Elixir.Hex.SCM'" ], "packageName": "hexpm/hex", "packageURL": "pkg:github/hexpm/hex", "product": "hex", "programFiles": [ "lib/hex/remote_converger.ex", "lib/hex/mix.ex", "lib/hex/scm.ex" ], "programRoutines": [ { "name": "'Elixir.Hex.RemoteConverger':deps/2" }, { "name": "'Elixir.Hex.RemoteConverger':converge/2" }, { "name": "'Elixir.Hex.Mix':to_lock/1" }, { "name": "'Elixir.Hex.SCM':update/1" }, { "name": "'Elixir.Hex.SCM':checkout/1" } ], "repo": "https://github.com/hexpm/hex", "vendor": "hexpm", "versions": [ { "changes": [ { "at": "1b3e2ae8f0dd7273a2c2625b9e50b6c67c8568fb", "status": "unaffected" }, { "at": "72279a8e2004b32b20d8767ff010c117193dd02c", "status": "unaffected" } ], "lessThan": "*", "status": "affected", "version": "380f87054e1bc08ee13d2b5311bc01f98dad2fc6", "versionType": "git" } ] } ], "configurations": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "

Affects projects whose dependency tree, directly or transitively, contains a package release whose registry metadata carries a crafted dependency app label, when dependencies are fetched with mix deps.get or mix deps.update. Packages on hex.pm are affected while hex.pm accepts such labels; packages on self-hosted repositories are affected when the repository operator signs such metadata.

" }, { "base64": false, "type": "text/markdown", "value": "Affects projects whose dependency tree, directly or transitively, contains a package release whose registry metadata carries a crafted dependency `app` label, when dependencies are fetched with `mix deps.get` or `mix deps.update`. Packages on hex.pm are affected while hex.pm accepts such labels; packages on self-hosted repositories are affected when the repository operator signs such metadata." } ], "value": "Affects projects whose dependency tree, directly or transitively, contains a package release whose registry metadata carries a crafted dependency app label, when dependencies are fetched with mix deps.get or mix deps.update. Packages on hex.pm are affected while hex.pm accepts such labels; packages on self-hosted repositories are affected when the repository operator signs such metadata." } ], "cpeApplicability": [ { "nodes": [ { "cpeMatch": [ { "criteria": "cpe:2.3:a:hexpm:hex:*:*:*:*:*:*:*:*", "versionEndExcluding": "2.5.2", "versionStartIncluding": "0.6.0", "vulnerable": true } ], "negate": false, "operator": "OR" } ], "operator": "AND" } ], "credits": [ { "lang": "en", "type": "finder", "value": "Peter Ullrich" }, { "lang": "en", "type": "remediation developer", "value": "Eric Meadows-Jönsson" }, { "lang": "en", "type": "analyst", "value": "Jonatan Männchen / EEF" } ], "dateAssigned": "2026-10-07T22:03:18.000Z", "descriptions": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability in hexpm hex allows a malicious package publisher to delete a directory outside the project's deps directory and extract package files into it when a developer fetches dependencies.

\n

A package release's requirements carry an app label that names the dependency's OTP application. Hex takes this label from the repository's signed registry and uses it as the dependency's application name without validating it (Hex.RemoteConverger.deps/2, Hex.Mix.to_lock/1). Mix derives the dependency's checkout directory as deps/<app>, and Hex.SCM.update/1 runs File.rm_rf!/1 on that directory and unpacks the package into it. hex.pm only checks the label's length, so a publisher can set it to a value such as ../../target. When a project depends on such a release directly or transitively, mix deps.get or mix deps.update deletes the directory the label resolves to and writes the labelled package's files there. This happens before any dependency code is compiled.

\n

This issue affects hex: from 0.6.0 before 2.5.2.

" }, { "base64": false, "type": "text/markdown", "value": "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability in hexpm hex allows a malicious package publisher to delete a directory outside the project's `deps` directory and extract package files into it when a developer fetches dependencies.\n\nA package release's requirements carry an `app` label that names the dependency's OTP application. Hex takes this label from the repository's signed registry and uses it as the dependency's application name without validating it (`Hex.RemoteConverger.deps/2`, `Hex.Mix.to_lock/1`). Mix derives the dependency's checkout directory as `deps/`, and `Hex.SCM.update/1` runs `File.rm_rf!/1` on that directory and unpacks the package into it. hex.pm only checks the label's length, so a publisher can set it to a value such as `../../target`. When a project depends on such a release directly or transitively, `mix deps.get` or `mix deps.update` deletes the directory the label resolves to and writes the labelled package's files there. This happens before any dependency code is compiled.\n\nThis issue affects hex: from 0.6.0 before 2.5.2." } ], "value": "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability in hexpm hex allows a malicious package publisher to delete a directory outside the project's deps directory and extract package files into it when a developer fetches dependencies.\n\nA package release's requirements carry an app label that names the dependency's OTP application. Hex takes this label from the repository's signed registry and uses it as the dependency's application name without validating it (Hex.RemoteConverger.deps/2, Hex.Mix.to_lock/1). Mix derives the dependency's checkout directory as deps/, and Hex.SCM.update/1 runs File.rm_rf!/1 on that directory and unpacks the package into it. hex.pm only checks the label's length, so a publisher can set it to a value such as ../../target. When a project depends on such a release directly or transitively, mix deps.get or mix deps.update deletes the directory the label resolves to and writes the labelled package's files there. This happens before any dependency code is compiled.\n\nThis issue affects hex: from 0.6.0 before 2.5.2." } ], "impacts": [ { "capecId": "CAPEC-126", "descriptions": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "

A publisher of any package in a project's dependency tree can make mix deps.get delete a directory that the developer's account can write and that .. segments relative to deps reach, such as the project directory or its parent, and fill it with the labelled package's files. Writing attacker-chosen files into a directory whose contents are later run or loaded can lead to code execution without the developer compiling the dependency. Developers who fetch dependencies to review them before compiling, CI jobs and dependency update bots that only run mix deps.get or mix deps.update are affected.

" }, { "base64": false, "type": "text/markdown", "value": "A publisher of any package in a project's dependency tree can make `mix deps.get` delete a directory that the developer's account can write and that `..` segments relative to `deps` reach, such as the project directory or its parent, and fill it with the labelled package's files. Writing attacker-chosen files into a directory whose contents are later run or loaded can lead to code execution without the developer compiling the dependency. Developers who fetch dependencies to review them before compiling, CI jobs and dependency update bots that only run `mix deps.get` or `mix deps.update` are affected." } ], "value": "A publisher of any package in a project's dependency tree can make mix deps.get delete a directory that the developer's account can write and that .. segments relative to deps reach, such as the project directory or its parent, and fill it with the labelled package's files. Writing attacker-chosen files into a directory whose contents are later run or loaded can lead to code execution without the developer compiling the dependency. Developers who fetch dependencies to review them before compiling, CI jobs and dependency update bots that only run mix deps.get or mix deps.update are affected." } ] } ], "metrics": [ { "cvssV4_0": { "Automatable": "NOT_DEFINED", "Recovery": "NOT_DEFINED", "Safety": "NOT_DEFINED", "attackComplexity": "LOW", "attackRequirements": "PRESENT", "attackVector": "NETWORK", "baseScore": 7.5, "baseSeverity": "HIGH", "privilegesRequired": "NONE", "providerUrgency": "NOT_DEFINED", "subAvailabilityImpact": "NONE", "subConfidentialityImpact": "NONE", "subIntegrityImpact": "NONE", "userInteraction": "ACTIVE", "valueDensity": "NOT_DEFINED", "vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", "version": "4.0", "vulnAvailabilityImpact": "HIGH", "vulnConfidentialityImpact": "HIGH", "vulnIntegrityImpact": "HIGH", "vulnerabilityResponseEffort": "NOT_DEFINED" }, "format": "CVSS", "scenarios": [ { "lang": "en", "value": "GENERAL" } ] } ], "problemTypes": [ { "descriptions": [ { "cweId": "CWE-22", "description": "CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", "lang": "en", "type": "CWE" } ] } ], "providerMetadata": { "orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db", "shortName": "EEF", "dateUpdated": "2026-10-12T15:39:49.862Z" }, "references": [ { "name": "GitHub Advisory", "tags": [ "related", "vendor-advisory" ], "url": "https://github.com/hexpm/hex/security/advisories/GHSA-2vjj-c5hr-6xc3" }, { "name": "EEF CNA record for CVE-2026-96612", "tags": [ "related" ], "url": "https://cna.erlef.org/cves/CVE-2026-96612.html" }, { "name": "OSV record EEF-CVE-2026-96612", "tags": [ "related" ], "url": "https://osv.dev/vulnerability/EEF-CVE-2026-96612" }, { "name": "Introducing commit 380f870 in hexpm/hex", "tags": [ "related" ], "url": "https://github.com/hexpm/hex/commit/380f87054e1bc08ee13d2b5311bc01f98dad2fc6" }, { "name": "Fix commit 1b3e2ae in hexpm/hex", "tags": [ "patch" ], "url": "https://github.com/hexpm/hex/commit/1b3e2ae8f0dd7273a2c2625b9e50b6c67c8568fb" }, { "name": "Fix commit 72279a8 in hexpm/hex", "tags": [ "patch" ], "url": "https://github.com/hexpm/hex/commit/72279a8e2004b32b20d8767ff010c117193dd02c" } ], "solutions": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "

Upgrade Hex to 2.5.2 or later, for example with mix local.hex. Hex then skips package releases that give a dependency an application name other than a letter followed by letters, digits, _ and -, and warns about them. Resolution fails if no other release of the package fits the requirements.

" }, { "base64": false, "type": "text/markdown", "value": "Upgrade Hex to 2.5.2 or later, for example with `mix local.hex`. Hex then skips package releases that give a dependency an application name other than a letter followed by letters, digits, `_` and `-`, and warns about them. Resolution fails if no other release of the package fits the requirements." } ], "value": "Upgrade Hex to 2.5.2 or later, for example with mix local.hex. Hex then skips package releases that give a dependency an application name other than a letter followed by letters, digits, _ and -, and warns about them. Resolution fails if no other release of the package fits the requirements." } ], "source": { "discovery": "UNKNOWN" }, "title": "Path traversal in Hex via unvalidated dependency app names", "workarounds": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "

Before running mix deps.get on a mix.lock from an untrusted change, check that every key in mix.lock is a plain application name without /, \\ or ... There's no workaround for new dependency resolution with mix deps.update or mix deps.get without a lock entry.

" }, { "base64": false, "type": "text/markdown", "value": "Before running `mix deps.get` on a `mix.lock` from an untrusted change, check that every key in `mix.lock` is a plain application name without `/`, `\\` or `..`. There's no workaround for new dependency resolution with `mix deps.update` or `mix deps.get` without a lock entry." } ], "value": "Before running mix deps.get on a mix.lock from an untrusted change, check that every key in mix.lock is a plain application name without /, \\ or ... There's no workaround for new dependency resolution with mix deps.update or mix deps.get without a lock entry." } ], "x_proofOfConcept": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "
    \n
  1. Publish package beta.
  2. \n
  3. Publish package alpha with a requirement on beta whose app is ../../escape_marker.
  4. \n
  5. In a project that depends on alpha, create ../escape_marker/canary.txt next to the project directory.
  6. \n
  7. Run mix deps.get. Hex prints * Updating ../../escape_marker (Hex package), deletes canary.txt and writes beta's files into ../escape_marker.
  8. \n
" }, { "base64": false, "type": "text/markdown", "value": "1. Publish package `beta`.\n2. Publish package `alpha` with a requirement on `beta` whose `app` is `../../escape_marker`.\n3. In a project that depends on `alpha`, create `../escape_marker/canary.txt` next to the project directory.\n4. Run `mix deps.get`. Hex prints `* Updating ../../escape_marker (Hex package)`, deletes `canary.txt` and writes `beta`'s files into `../escape_marker`." } ], "value": "* Publish package beta.\n* Publish package alpha with a requirement on beta whose app is ../../escape_marker.\n* In a project that depends on alpha, create ../escape_marker/canary.txt next to the project directory.\n* Run mix deps.get. Hex prints * Updating ../../escape_marker (Hex package), deletes canary.txt and writes beta's files into ../escape_marker." } ], "x_technicalAnalysis": [ { "lang": "en", "supportingMedia": [ { "base64": false, "type": "text/html", "value": "
    \n
  1. hex.pm stores each requirement's app field with only a length check (255 codepoints) and copies it into the signed package registry resource when it differs from the dependency's package name.
  2. \n
  3. Hex.RemoteConverger.deps/2 returns the dependency's children with String.to_atom/1 of that label as the application name, and Hex.Mix.to_lock/1 writes it as the mix.lock key.
  4. \n
  5. Mix computes the dependency destination as Path.join(Mix.Project.deps_path(), Atom.to_string(app)) without validating the name.
  6. \n
  7. Hex.SCM.update/1 calls File.rm_rf!(dest) and then Hex.Tar.unpack!(path, dest), so a label containing .. segments deletes and repopulates a directory outside deps.
  8. \n
  9. Mix doesn't evaluate a Hex dependency's mix.exs during mix deps.get, so this is reachable before any package code runs.
  10. \n
" }, { "base64": false, "type": "text/markdown", "value": "1. hex.pm stores each requirement's `app` field with only a length check (255 codepoints) and copies it into the signed package registry resource when it differs from the dependency's package name.\n2. `Hex.RemoteConverger.deps/2` returns the dependency's children with `String.to_atom/1` of that label as the application name, and `Hex.Mix.to_lock/1` writes it as the `mix.lock` key.\n3. Mix computes the dependency destination as `Path.join(Mix.Project.deps_path(), Atom.to_string(app))` without validating the name.\n4. `Hex.SCM.update/1` calls `File.rm_rf!(dest)` and then `Hex.Tar.unpack!(path, dest)`, so a label containing `..` segments deletes and repopulates a directory outside `deps`.\n5. Mix doesn't evaluate a Hex dependency's `mix.exs` during `mix deps.get`, so this is reachable before any package code runs." } ], "value": "* hex.pm stores each requirement's app field with only a length check (255 codepoints) and copies it into the signed package registry resource when it differs from the dependency's package name.\n* Hex.RemoteConverger.deps/2 returns the dependency's children with String.to_atom/1 of that label as the application name, and Hex.Mix.to_lock/1 writes it as the mix.lock key.\n* Mix computes the dependency destination as Path.join(Mix.Project.deps_path(), Atom.to_string(app)) without validating the name.\n* Hex.SCM.update/1 calls File.rm_rf!(dest) and then Hex.Tar.unpack!(path, dest), so a label containing .. segments deletes and repopulates a directory outside deps.\n* Mix doesn't evaluate a Hex dependency's mix.exs during mix deps.get, so this is reachable before any package code runs." } ] } } }