CVE
CVE-2026-10796
CWE
CWE-78
Sistemas afectados
- nvm 0.40.4 y anteriores
- Estaciones de trabajo de desarrolladores que ejecutan nvm contra mirrors personalizados de Node.js o io.js
- Trabajos de CI/CD que definen NVM_NODEJS_ORG_MIRROR o NVM_IOJS_ORG_MIRROR
- Infraestructura de mirrors corporativa o aislada (air-gapped) que sirve metadatos index.tab
El 4 de junio de 2026, los mantenedores de nvm publicaron GHSA-3c52-35h2-gfmm y lanzaron la v0.40.5 para CVE-2026-10796. El problema no es “solo otro fallo de escapado en shell”. Es un fallo de límite de confianza en una de las herramientas de arranque para desarrolladores más comunes: nvm aceptaba cadenas de versión del index.tab de un mirror configurado de Node.js o io.js, y luego reutilizaba esos datos no confiables en contextos evaluados por shell y por awk.
Eso importa a los equipos de seguridad de aplicaciones porque muchas organizaciones no usan directamente el mirror público https://nodejs.org/dist. Usan capas de caché internas, proxies de artefactos, mirrors aislados, mirrors específicos de CI o imágenes de incorporación de desarrolladores que preconfiguran NVM_NODEJS_ORG_MIRROR. En esos entornos, un mirror comprometido, un proxy envenenado o un ataque de intermediario sin TLS puede convertir un nvm install rutinario en ejecución de código arbitrario en estaciones de trabajo de desarrolladores y ejecutores de compilación.
Proyectos y entornos afectados
El componente vulnerable es:
nvm<= 0.40.4
Los entornos que más importan son:
- perfiles de shell que exportan
NVM_NODEJS_ORG_MIRRORoNVM_IOJS_ORG_MIRROR - pipelines de CI que ejecutan
nvm install,nvm useonvm ls-remotecontra un mirror interno - estaciones de trabajo de desarrolladores que heredan la configuración de mirror desde dotfiles, devcontainers, imágenes Docker o scripts de arranque
El mirror predeterminado sigue siendo de menor riesgo porque la ruta normal usa https://nodejs.org sobre TLS. La ventana de exposición se abre cuando los equipos sustituyen ese valor predeterminado por infraestructura de mirror propia o por endpoints http:// inseguros.
El flujo de datos vulnerable
La causa raíz del aviso es una cadena simple:
mirror/index.tab
-> nvm_ls_remote_index_tab()
-> nvm_remote_version()
-> nvm_download_artifact()
-> nvm_download() / nvm_get_checksum()
-> ejecución de comandos local
nvm trataba el primer campo de index.tab como un identificador de versión. Esa versión luego se incrustaba en URLs, slugs y cadenas de comandos sin una comprobación final de “¿son seguros de evaluar estos datos?”.
Dos sumideros distintos eran alcanzables desde el mismo campo de versión controlado por el atacante.
Sumidero 1: ejecución de shell mediante eval en nvm_download()
Antes de la v0.40.5, nvm_download() construía una cadena y la ejecutaba con 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}"
Ese patrón es peligroso porque las comillas dobles no neutralizan la sustitución de comandos. Si un mirror suministraba una versión como:
v99.99.99$(id>/tmp/pwned_nvm)
entonces un comando normal como:
nvm install node
podía resolver una fila de versión maliciosa, construir una URL de descarga que contuviera $(...), y ejecutar la carga útil localmente antes incluso de que curl o wget obtuvieran el tarball. El reproductor de la GHSA muestra exactamente ese comportamiento falseando la respuesta index.tab del mirror y demostrando que el comando id se ejecutó en el host víctima.
Esta es la lección clave: la entrada peligrosa no es un argumento de CLI escrito por el usuario. Es metadata entregada por un mirror upstream y luego reinterpretada como sintaxis de shell.
Sumidero 2: inyección de código awk en nvm_get_checksum()
El segundo sumidero es posiblemente más interesante porque sobrevive incluso después de que los defensores aprendan a buscar eval en crudo.
Antes del parche, nvm_get_checksum() interpolaba el slug del tarball derivado de la versión directamente en el texto del programa awk:
nvm_download -L -s "${SHASUMS_URL}" -o - | command awk "{ if (\"${4}.${5}\" == \$2) print \$1}"
Como el slug incrusta la versión suministrada por el mirror, un valor manipulado podía escapar del literal de cadena e invocar system() de awk:
v1"==$2){system("touch${IFS}/tmp/pwned_nvm")}#
Eso importa porque crea una segunda ruta de explotación en el flujo de validación de checksums. Aunque un equipo modele mentalmente el fallo como “un problema de eval”, el problema real es más amplio: metadatos de mirror no confiables llegaban a múltiples evaluadores.
Qué cambió en la v0.40.5
La corrección es limpia y merece la pena entenderla porque elimina todo el patrón inseguro en lugar de intentar poner en lista negra unos pocos caracteres a última hora.
Primero, el commit 6d870d18 elimina eval de la ruta del descargador y pasa los argumentos como elementos literales de argv:
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
set -- "$@" --header "Authorization: ${sanitized_header}"
fi
"${NVM_DOWNLOADER}" "$@"
Eso importa porque el shell ya no vuelve a analizar una cadena de comando construida. La URL suministrada por el mirror es solo datos.
Segundo, el commit 90bb8874 deja de incrustar el nombre del tarball dentro del programa awk y lo pasa con -v en su lugar:
nvm_download -L -s "${SHASUMS_URL}" -o - | command awk -v tarball="${4}.${5}" '{ if (tarball == $2) print $1 }'
Eso convierte de nuevo el nombre del tarball en datos en lugar de código fuente awk ejecutable.
Tercero, el commit 70fb4ede añade una comprobación de caracteres de versión en 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
Este es un cambio importante de defensa en profundidad. Aunque código futuro reintroduzca accidentalmente un evaluador más adelante en el flujo, los metacaracteres obvios de shell y awk ya no pasan sin comprobación a través de la gramática de versión.
Por qué esto es un problema de cadena de suministro, no solo un fallo de shell local
El atacante no necesita acceso de escritura al repositorio de la víctima. Necesita influir en los metadatos del mirror en los que nvm confía. En la práctica, eso puede ocurrir mediante:
- el compromiso de un mirror de artefactos interno
- imágenes de arranque maliciosas o mal configuradas que exportan variables de mirror personalizadas
- plantillas de CI o imágenes de ejecutores autogestionados que sobrescriben los endpoints de mirror de
nvm - mirrors en texto plano
http://que permiten la manipulación de respuestas en tránsito
Eso convierte a CVE-2026-10796 en un problema de cadena de suministro de herramientas de desarrollo. La superficie de explotación se sitúa entre los metadatos de versión upstream y la ejecución de compilación downstream, exactamente donde muchas organizaciones han añadido caché y mirroring privados para “hacer las compilaciones más fiables”.
Delimitación y detección
Empieza por encontrar dónde tus entornos sobrescriben el 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"
Luego identifica la versión de nvm desplegada:
command -v nvm >/dev/null 2>&1 && nvm --version
Revisa los repositorios de arranque de shell, las imágenes doradas y las plantillas de CI en busca de cualquier mirror http:// o cualquier mirror cuya integridad no se valide de forma independiente. Si operas un mirror interno, inspecciona el historial almacenado de index.tab en busca de caracteres inesperados en la columna de versión y compáralo con los metadatos de lanzamiento upstream de Node.js.
Para el análisis forense de compilaciones, busca ejecuciones de nvm install o nvm ls-remote que ocurrieron mientras un mirror personalizado estaba configurado:
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
Guía de respuesta
Si dependes de mirrors personalizados, trata esto como exposición a ejecución de código en cada host que ejecutó comandos vulnerables de nvm contra esos mirrors.
- Actualiza
nvma la0.40.5. - Vuelve al mirror predeterminado respaldado por TLS a menos que exista una razón operativa fuerte para no hacerlo.
- Valida que los mirrors corporativos sirvan datos de
index.tab, tarball y checksum sin modificar desde fuentes upstream confiables. - Revisa los scripts de arranque de CI y de estaciones de trabajo en busca de variables de mirror exportadas heredadas de imágenes base o dotfiles.
- Si un mirror personalizado pudo haber estado controlado por un atacante, rota las credenciales alcanzables desde el host afectado y revisa los artefactos de compilación producidos durante la ventana de exposición.
La lección más profunda es arquitectónica: las herramientas de desarrollo que analizan “metadatos de versión” siguen analizando entradas controlables por el atacante. Si esos datos llegan a eval, a la expansión de shell, a la interpretación de plantillas o a runtimes de lenguajes embebidos como awk, la infraestructura de mirror se convierte en parte de tu límite de seguridad de aplicaciones, lo hayas pretendido o no.
De la investigación a la remediación
Comprueba si este patrón existe en tu código
Convierte esta investigación en un flujo de remediación. Analiza dependencias y manifiestos para detectar riesgos similares en la cadena de suministro y prioriza las correcciones con contexto de alcanzabilidad.