high

CVE

CVE-2026-10796

CWE

CWE-78

Affected Surface

nvm 0.40.4 and earlier, Developer workstations that run nvm against custom Node.js or io.js mirrors, CI/CD jobs that set NVM_NODEJS_ORG_MIRROR or NVM_IOJS_ORG_MIRROR, Corporate or air-gapped mirror infrastructure serving index.tab metadata

On 4 June 2026, the nvm maintainers published GHSA-3c52-35h2-gfmm and released v0.40.5 for CVE-2026-10796. The issue is not “just another shell quoting bug.” It is a trust-boundary failure in one of the most common developer bootstrap tools: nvm accepted version strings from a configured Node.js or io.js mirror’s index.tab, then reused that untrusted data in shell-evaluated and awk-evaluated contexts.

That matters for application security teams because many organizations do not use the public https://nodejs.org/dist mirror directly. They use internal cache layers, artifact proxies, air-gapped mirrors, CI-specific mirrors, or developer onboarding images that preconfigure NVM_NODEJS_ORG_MIRROR. In those environments, a compromised mirror, poisoned proxy, or non-TLS man-in-the-middle can turn a routine nvm install into arbitrary code execution on developer workstations and build runners.

Affected projects and environments

The vulnerable component is:

  • nvm <= 0.40.4

The environments that matter most are:

  • shell profiles that export NVM_NODEJS_ORG_MIRROR or NVM_IOJS_ORG_MIRROR
  • CI pipelines that run nvm install, nvm use, or nvm ls-remote against an internal mirror
  • developer workstations that inherit mirror settings from dotfiles, devcontainers, Docker images, or bootstrap scripts

The default mirror remains lower risk because the normal path uses https://nodejs.org over TLS. The exposure window opens when teams replace that default with mirror infrastructure they control or with insecure http:// endpoints.

The vulnerable data flow

The advisory’s root cause is a simple chain:

mirror/index.tab
  -> nvm_ls_remote_index_tab()
  -> nvm_remote_version()
  -> nvm_download_artifact()
  -> nvm_download() / nvm_get_checksum()
  -> local command execution

nvm treated the first field in index.tab as a version identifier. That version was then embedded into URLs, slugs, and command strings without a final “is this data safe to evaluate?” boundary check.

Two distinct sinks were reachable from the same attacker-controlled version field.

Sink 1: shell execution through eval in nvm_download()

Before v0.40.5, nvm_download() built a string and ran it with eval:

for arg in "$@"; do
  NVM_DOWNLOAD_ARGS="${NVM_DOWNLOAD_ARGS} \"$arg\""
done
eval "curl -q --fail ${CURL_COMPRESSED_FLAG:-} ${CURL_HEADER_FLAG:-} ${NVM_DOWNLOAD_ARGS}"

That pattern is dangerous because double quotes do not neutralize command substitution. If a mirror supplied a version such as:

v99.99.99$(id>/tmp/pwned_nvm)

then a normal command such as:

nvm install node

could resolve a malicious version row, build a download URL containing $(...), and execute the payload locally before curl or wget even fetched the tarball. The GHSA reproducer shows exactly that behavior by faking the mirror’s index.tab response and proving that the id command ran on the victim host.

This is the key lesson: the dangerous input is not a CLI argument typed by the user. It is metadata delivered by an upstream mirror and then reinterpreted as shell syntax.

Sink 2: awk code injection in nvm_get_checksum()

The second sink is arguably more interesting because it survives even after defenders learn to search for raw eval.

Before the patch, nvm_get_checksum() interpolated the version-derived tarball slug directly into the awk program text:

nvm_download -L -s "${SHASUMS_URL}" -o - | command awk "{ if (\"${4}.${5}\" == \$2) print \$1}"

Because the slug embeds the mirror-supplied version, a crafted value could break out of the string literal and invoke awk’s system():

v1"==$2){system("touch${IFS}/tmp/pwned_nvm")}#

That matters because it creates a second exploit path in the checksum-validation flow. Even if a team mentally models the bug as “an eval problem,” the real issue is broader: untrusted mirror metadata reached multiple evaluators.

What changed in v0.40.5

The fix is clean and worth understanding because it removes the entire unsafe pattern rather than trying to blacklist a few characters at the last minute.

First, commit 6d870d18 removes eval from the downloader path and passes arguments as literal argv elements:

if [ -n "${NVM_AUTH_HEADER:-}" ]; then
  set -- "$@" --header "Authorization: ${sanitized_header}"
fi

"${NVM_DOWNLOADER}" "$@"

That matters because the shell no longer reparses a constructed command string. The mirror-supplied URL is just data.

Second, commit 90bb8874 stops embedding the tarball name inside the awk program and passes it with -v instead:

nvm_download -L -s "${SHASUMS_URL}" -o - | command awk -v tarball="${4}.${5}" '{ if (tarball == $2) print $1 }'

That turns the tarball name back into data instead of executable awk source.

Third, commit 70fb4ede adds a version-character guard in nvm_download_artifact():

case "${VERSION}" in
  '')
    nvm_err 'A version number is required.'
    return 3
  ;;
  *[!0-9A-Za-z._+-]*)
    nvm_err 'Invalid version: contains disallowed characters'
    return 3
  ;;
esac

This is an important defense-in-depth change. Even if future code accidentally reintroduces an evaluator downstream, obvious shell and awk metacharacters no longer pass through the version grammar unchecked.

Why this is a supply-chain problem, not just a local shell bug

The attacker does not need write access to the victim’s repo. They need influence over the mirror metadata that nvm trusts. In practice, that can happen through:

  • compromise of an internal artifact mirror
  • malicious or misconfigured bootstrap images that export custom mirror variables
  • CI templates or self-hosted runner images that override nvm mirror endpoints
  • plaintext http:// mirrors that allow response tampering in transit

That makes CVE-2026-10796 a developer-tooling supply-chain issue. The exploit surface sits between upstream version metadata and downstream build execution, exactly where many organizations have added private caching and mirroring to “make builds more reliable.”

Scoping and detection

Start by finding where your environments override the mirror:

rg -n 'NVM_(NODEJS|IOJS)_ORG_MIRROR' \
  .github .gitlab-ci.yml .circleci Dockerfile docker-compose.yml devcontainer.json \
  scripts .env* "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.profile"

Then identify the deployed nvm version:

command -v nvm >/dev/null 2>&1 && nvm --version

Review shell bootstrap repositories, golden images, and CI templates for any http:// mirror or any mirror whose integrity is not independently validated. If you operate an internal mirror, inspect stored index.tab history for unexpected characters in the version column and compare it against upstream Node.js release metadata.

For build forensics, look for nvm install or nvm ls-remote executions that occurred while a custom mirror was configured:

rg -n 'nvm (install|ls-remote|use)' .github/workflows .gitlab-ci.yml .circleci scripts
rg -n 'NVM_(NODEJS|IOJS)_ORG_MIRROR' .github/workflows .gitlab-ci.yml .circleci scripts

Response guidance

If you rely on custom mirrors, treat this as code execution exposure on every host that ran vulnerable nvm commands against those mirrors.

  1. Upgrade nvm to 0.40.5.
  2. Revert to the default TLS-backed mirror unless there is a strong operational reason not to.
  3. Validate that corporate mirrors serve unmodified index.tab, tarball, and checksum data from trusted upstream sources.
  4. Review CI and workstation bootstrap scripts for exported mirror variables inherited from base images or dotfiles.
  5. If a custom mirror might have been attacker-controlled, rotate credentials reachable from the affected host and review build outputs produced during the exposure window.

The deeper lesson is architectural: developer tools that parse “version metadata” are still parsing attacker-controllable input. If that data reaches eval, shell expansion, template interpretation, or embedded language runtimes like awk, mirror infrastructure becomes part of your application security boundary whether you intended it or not.

References