Alex HerreraDesarrollador de software · Enfoque en ciberseguridad
EN
← Volver al blog

Seguridad web

Diferencia entre npm y pnpm en la seguridad al instalar

Cuatro comportamientos de npm y pnpm al instalar, medidos versión por versión, y la configuración que los iguala.

  • Seguridad
  • Infraestructura
Publicado
Actualizado
Lectura
18 min de lectura

Instalar una dependencia ejecuta código antes de que el proyecto la importe. Ese código está en los scripts de instalación, órdenes que el paquete declara en su package.json para que el gestor las ejecute al instalarlo. Nadie ha revisado todavía el paquete, y el script se ejecuta con los permisos de la cuenta que lanzó la instalación.

Los dos gestores ejecutaban esos scripts sin pedir confirmación, y los dos han dejado de hacerlo. El primero fue pnpm 10, publicado el 7 de enero de 2025, que lo anunció como un cambio incompatible hecho para aumentar la seguridad1. La versión 11 convirtió el aviso en un error y añadió una segunda defensa. En julio de 2026, npm 12 hizo lo mismo. Bloquea los scripts de instalación de las dependencias, salvo los de los paquetes nombrados en un campo nuevo, allowScripts2.

Por eso, para comparar npm y pnpm hay que comparar versiones concretas. De las cuatro versiones medidas, dos bloquean los scripts con un aviso y sin detener la instalación. Una los bloquea con un error, y la cuarta los ejecuta.

La tabla compara npm 11.8.0 y npm 12.0.2 con pnpm 10.0.0 y pnpm 11.26.0 en cuatro comportamientos, sin configurar nada. El punto 7 reúne la configuración que da el mismo resultado en una versión de npm anterior a la 12. Cada celda sale de ejecutar la orden, salvo el 1440 de la última fila, que es el valor que declara la documentación de pnpm. Cada proyecto de ejemplo cabe en uno o dos archivos, que aparecen completos en su punto.

Comportamiento npm 11.8.0 npm 12.0.2 pnpm 10.0.0 pnpm 11.26.0
postinstall de una dependencia se ejecuta bloqueado, aviso, salida 0 bloqueado, aviso, salida 0 bloqueado, salida 1
require de un paquete no declarado resuelve resuelve MODULE_NOT_FOUND MODULE_NOT_FOUND
Instalación en CI con el bloqueo desfasado instala otra versión, salida 0 instala otra versión, salida 0 salida 1 salida 1
Espera antes de resolver una versión recién publicada el ajuste no existe ajuste presente, sin valor ninguna 1440 minutos

La tabla mide valores por defecto y no capacidades. El valor por defecto de la primera fila cambia con la versión mayor del gestor. Por eso lo primero es saber qué versión ejecuta la máquina. La fila 1 se iguala en npm 12 con strict-allow-scripts=true, que hace terminar la instalación en 1 (punto 1). Las filas 3 y 4 se igualan con npm ci en la integración continua y con un ajuste en .npmrc (punto 7). La fila 2 no se puede igualar, porque depende de cómo coloca cada gestor los paquetes en el disco.

1. Qué código de terceros ejecuta la instalación

Un paquete puede declarar un script que se ejecuta al instalarlo. Para verlo basta un proyecto de dos archivos. La dependencia apunta con file: a una carpeta local, así que la instalación no descarga nada de la red.

// package.json
{
  "name": "prueba-instalacion",
  "version": "1.0.0",
  "private": true,
  "dependencies": { "evil-postinstall": "file:./evil-postinstall" }
}
// evil-postinstall/package.json
{
  "name": "evil-postinstall",
  "version": "1.0.0",
  "scripts": { "postinstall": "node -e \"require('fs').writeFileSync('EJECUTADO.txt','')\"" }
}

El nombre evil-postinstall está inventado para el artículo y no existe en el registro público de npm. Se comprobó el 7 de septiembre de 2026 con npm view evil-postinstall, que devuelve E404.

Con npm 11.8.0, la instalación termina bien y el archivo aparece, sin que nada en la salida lo mencione:

npm install
# salida 0
ls evil-postinstall/EJECUTADO.txt

Con npm 12.0.2, la misma orden sobre el mismo proyecto no crea el archivo. Muestra un aviso y termina con el código de salida 0, que indica que la orden se completó sin error:

npm install
# npm warn install-scripts 1 package had install scripts blocked because they are
# npm warn install-scripts not covered by allowScripts:
# npm warn install-scripts   evil-postinstall@1.0.0 (postinstall: node -e "...")
# npm warn install-scripts Run `npm install-scripts ls` to review, or
# npm warn install-scripts `npm install-scripts approve <pkg>` to allow.
# salida 0

Un código 0 no detiene la integración continua, el servidor que ejecuta las comprobaciones en cada cambio. Por eso este bloqueo protege la máquina, pero no obliga a nadie a revisar la lista. Las dos órdenes del aviso muestran la lista y aprueban un paquete, y el permiso queda en allowScripts, dentro de package.json2.

Para que npm 12 detenga la instalación, active strict-allow-scripts. Con npm 12.1.0, la misma orden termina así:

npm install --strict-allow-scripts=true
# npm error code ESTRICTALLOWSCRIPTS
# npm error --strict-allow-scripts: 1 package(s) have install scripts not covered by allowScripts:
# npm error   evil-postinstall@1.0.0 (postinstall: node -e "...")
# salida 1

Escriba strict-allow-scripts=true en el .npmrc del proyecto para que se aplique en todas las máquinas.

Con pnpm 11, la instalación se detiene antes:

pnpm install
# [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: evil-postinstall@file:evil-postinstall
# Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
# salida 1

De las cuatro versiones medidas, pnpm 11 es la única que termina con un código distinto de 0 ante un script de instalación sin que nadie haya configurado nada.

Escribir un archivo vacío es inofensivo. Con el mismo permiso, un script puede leer variables de entorno, buscar credenciales en el disco o publicar paquetes con las credenciales de quien instala.

Ese último caso ocurrió en septiembre de 2025. El gusano Shai-Hulud se propagó por npm mediante un script postinstall, colocado en paquetes de mantenedores comprometidos3. CISA llegó a publicar una alerta4.

El bloqueo por defecto protege también de un paquete malicioso que nadie ha detectado todavía, porque no necesita reconocerlo. Limita lo que cualquier paquete puede hacer mientras se instala.

En una versión de npm anterior a la 12, consiga el mismo bloqueo con una línea en .npmrc, el archivo de configuración del proyecto. El ajuste ignore-scripts existe desde hace años y su valor por defecto es false6. En npm 12 sigue siendo false: el bloqueo de allowScripts es un mecanismo nuevo y paralelo, y no cambia el valor de ese ajuste.

# .npmrc
ignore-scripts=true

Con esa línea, la instalación del proyecto anterior termina sin crear EJECUTADO.txt.

El ajuste afecta también a los scripts del propio proyecto. La documentación de npm indica que npm run, npm test y npm start siguen ejecutando el script que se pide, pero no sus scripts pre y post6. En la medición, con npm 11.8.0, ignore-scripts=true tampoco ejecutó el prepare del proyecto durante npm install. Si el proyecto usa prepare para instalar los hooks de git, como hace husky, esos hooks dejan de instalarse. Ejecute ese paso aparte, con npm run prepare.

Bloquear los scripts tiene un coste: paquetes como esbuild, puppeteer o sqlite3 descargan o compilan su binario durante la instalación, y sin él no funcionan. Ejecute el script de cada uno por separado, sin el bloqueo y solo para esa orden:

npm rebuild esbuild --ignore-scripts=false

La opción --ignore-scripts=false es necesaria, porque con ignore-scripts=true en la configuración, un npm rebuild sin ella tampoco ejecuta nada.

2. Cómo resolver ERR_PNPM_IGNORED_BUILDS en pnpm 11 con allowBuilds

pnpm 10 dejó de ejecutar los scripts de las dependencias durante la instalación1. Muestra un aviso y termina con código 0, así que, igual que npm 12 (punto 1), no detiene la integración continua:

The following dependencies have build scripts that were ignored: esbuild

En esa versión, el permiso se concede en package.json, con el campo pnpm.onlyBuiltDependencies.

pnpm 11.0.0, del 28 de abril de 2026, cambió las dos cosas7. El aviso pasó a ser el error ERR_PNPM_IGNORED_BUILDS, con código de salida 1, y la instalación falla hasta que alguien decide qué scripts se permiten. El permiso también cambió de archivo. onlyBuiltDependencies, junto con onlyBuiltDependenciesFile, neverBuiltDependencies, ignoredBuiltDependencies e ignoreDepScripts, se eliminó, y lo sustituye allowBuilds en pnpm-workspace.yaml8. La versión 10.26.0 ya había añadido allowBuilds como alternativa a esos ajustes8, así que puede migrar antes de actualizar a la 11.

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true

Con esa entrada, la instalación vuelve a terminar en 0 y el script de esbuild se ejecuta.

Cuando pnpm 11 bloquea un script, escribe él mismo en pnpm-workspace.yaml una entrada con el valor pendiente:

allowBuilds:
  esbuild: set this to true or false

No es habitual que una instalación modifique un archivo versionado. A cambio, la decisión queda en el historial del repositorio en lugar de quedarse en la máquina de quien instaló. La orden pnpm approve-builds recorre la lista de forma interactiva y escribe las respuestas en ese mismo archivo8.

3. Cómo detectar las dependencias que el código usa sin declararlas

A diferencia de los scripts, este riesgo no necesita código malicioso y no produce ningún error. Un proyecto que solo declara debug recibe también ms, porque debug la necesita para funcionar. ms es una dependencia transitiva, es decir, una dependencia de una dependencia. La cuestión es si el código del propio proyecto puede importarla.

{
  "name": "fantasma",
  "version": "1.0.0",
  "private": true,
  "dependencies": { "debug": "4.3.4" }
}
node -e "require('ms')"

Con npm, la línea funciona en las dos versiones de la tabla. Con pnpm falla con MODULE_NOT_FOUND en las dos, porque pnpm solo deja en la raíz de node_modules las dependencias que declara el proyecto.

Si un require funciona sin que el paquete esté declarado, la versión de ese paquete la elige otro paquete. Nadie la fijó, nadie la revisa cuando se actualiza, y desaparece el día que debug cambie o sustituya su propia dependencia.

Cómo comprobarlo. Instale el proyecto con pnpm y arranque la aplicación. Cada MODULE_NOT_FOUND que aparezca es un paquete que faltaba declarar.

4. Cómo hacer que la instalación respete el archivo de bloqueo

El archivo de bloqueo (package-lock.json en npm, pnpm-lock.yaml en pnpm) fija la versión exacta de cada dependencia. Deja de servir en cuanto la orden que se ejecuta en el servidor puede reescribirlo.

Para reproducirlo, instale un proyecto con debug en 4.3.4. Después, cambie package.json a 4.4.0 sin tocar el archivo de bloqueo, que es lo que deja una rama mal fusionada. La variable CI=true simula el entorno de integración continua:

CI=true npm install
# salida 0
node -p "require('./node_modules/debug/package.json').version"
# 4.4.0

npm instala una versión que el archivo de bloqueo no nombra, actualiza el archivo para que coincida y termina en 0. Nada en la salida indica que la versión cambió. Con pnpm, el mismo caso termina así:

CI=true pnpm install
# ERR_PNPM_OUTDATED_LOCKFILE
# salida 1

pnpm se detiene y deja 4.3.4 en su sitio. La opción --frozen-lockfile, que impide modificar el archivo de bloqueo, vale true cuando pnpm detecta un entorno de integración continua, y false fuera de él9.

En npm, el equivalente es una orden aparte que hay que escribir: npm ci. Falla si el archivo de bloqueo no coincide con package.json, borra node_modules antes de empezar y nunca escribe en esos dos archivos10.

npm ci
# npm error code EUSAGE
# salida 1

Los dos gestores respetan el archivo de bloqueo cuando se les pide, y solo cambia lo que hacen si nadie escribe la orden explícita.

5. Cómo esperar antes de instalar una versión recién publicada

pnpm 11 cambió además un segundo valor por defecto, que npm 12 no iguala. Con él, pnpm no elige una versión recién publicada hasta que tiene al menos un día. El ajuste se llama minimumReleaseAge, se mide en minutos y pasó de 0 a 144011.

# pnpm-workspace.yaml
minimumReleaseAge: 1440

La espera responde a cómo ocurren estos ataques. Las versiones maliciosas de chalk y debug se publicaron en dos tandas separadas por minutos, y se descubrieron el mismo día5. Una espera de un día cubre ese intervalo, en el que el paquete comprometido ya está publicado y todavía nadie lo ha detectado.

Pedir una versión exacta no evita la espera. Con debug fijado a 4.4.3 y un minimumReleaseAge alto, la instalación falla:

Error: ERR_PNPM_NO_MATURE_MATCHING_VERSION
  × installing dependencies
  ╰─▶ 2 versions do not meet the minimumReleaseAge constraint:
        debug@4.4.3 was published at 2025-09-13T17:25:19.732Z, within the
      minimumReleaseAge cutoff
  help: Run the install interactively to approve these picks, add them to
        minimumReleaseAgeExclude in pnpm-workspace.yaml, or wait for the
        packages to mature.
# salida 1

Añada las excepciones a mano en minimumReleaseAgeExclude, como pide el mensaje. A diferencia de lo que hace con allowBuilds, pnpm no escribe estas entradas en el archivo. Otro ajuste, minimumReleaseAgeStrict, decide si pnpm falla o elige una versión anterior que cumpla la restricción11.

npm tiene un ajuste equivalente, min-release-age, desde la versión 11.10.0, con dos diferencias. Se mide en días y no en minutos, y viene sin valor, así que por defecto no espera nada. El ajuste min-release-age-exclude acepta nombres de paquete y patrones minimatch, que admiten comodines6.

npm config get min-release-age
# npm 11.8.0  -> undefined   (el ajuste no existe)
# npm 11.10.0 -> null        (existe, sin valor)
# npm 12.0.2  -> null

npm tiene además el ajuste before, que fija todas las resoluciones a una fecha concreta y que también viene sin valor.

6. Qué hacen igual los dos gestores

Los dos guardan en el archivo de bloqueo el hash de integridad de cada paquete. Si el contenido descargado no coincide, los dos se detienen. Si altera a mano una entrada del archivo de bloqueo, npm falla con EINTEGRITY y pnpm con ERR_PNPM_TARBALL_INTEGRITY. Los dos tienen una orden que obliga a respetar ese archivo (punto 4). Ninguno inspecciona lo que hace el código del paquete una vez instalado, que es el caso de chalk y debug (punto 1).

En sus versiones actuales, los dos bloquean los scripts de instalación de la misma forma. Usan una lista de paquetes autorizados, que se versiona con el repositorio, y una orden interactiva que la rellena. En npm, la lista es allowScripts en package.json, y la orden, npm install-scripts approve. En pnpm, la lista es allowBuilds en pnpm-workspace.yaml, y la orden, pnpm approve-builds. Cambia la gravedad por defecto: npm 12 avisa y termina en 0, y pnpm 11 termina en 1. Con strict-allow-scripts=true, npm 12 también termina en 1.

El registro de npm también cambia la forma de publicar paquetes. En septiembre de 2025, npm anunció la publicación con identidad verificada, llamada trusted publishing, y la retirada de los tokens clásicos. También anunció un límite de siete días para la vida de los tokens granulares y una caducidad más corta para los que pueden publicar12. Son cambios en cómo se publica un paquete, no en lo que ocurre al instalarlo, así que se suman a lo anterior.

7. Cómo configurar npm de forma segura

No hace falta cambiar de gestor. Lo que se puede igualar depende de la versión de npm:

Versión Scripts de instalación Espera mínima
npm 12 allowScripts por defecto; strict-allow-scripts=true para terminar en 1 min-release-age
npm 11.16 a 11.x allowScripts y strict-allow-scripts=true; sin configurar, avisa y ejecuta los scripts min-release-age
npm 11.10 a 11.15 ignore-scripts=true min-release-age
npm anterior a 11.10 ignore-scripts=true no existe

Compruebe primero la versión con npm --version. En npm 12, deje el bloqueo de allowScripts y añada la línea que detiene la instalación:

# .npmrc, npm 12
strict-allow-scripts=true

npm 11.16 añadió allowScripts y strict-allow-scripts, y npm 11.18, la orden npm install-scripts. En esas versiones, sin strict-allow-scripts=true la instalación avisa pero ejecuta los scripts, así que añada la misma línea. En una versión anterior a la 11.16, ningún ajuste da una lista de paquetes autorizados. Escriba ignore-scripts=true, el ajuste del punto 1, y confírmelo en el repositorio para que se aplique a todo el equipo. Tenga en cuenta que también desactiva el prepare del proyecto (punto 1):

# .npmrc, npm anterior a la 11.16
ignore-scripts=true

En el servidor, instale con npm ci. La orden respeta el archivo de bloqueo (punto 4) y también el .npmrc, así que aplica las dos protecciones:

npm ci

Con ignore-scripts=true, el proyecto del punto 1 se instala sin crear EJECUTADO.txt y termina en 0. Es el resultado de pnpm 10 y de npm 12 sin configurar, conseguido con otro ajuste.

Cómo comprobarlo. En la máquina que instala, compruebe que el ajuste está activo:

npm config get strict-allow-scripts   # npm 12, o 11.16 y posteriores
npm config get ignore-scripts         # npm anterior a la 11.16
# true

Compruebe también las firmas del registro. En un proyecto con dependencias reales, npm audit signatures confirma que lo descargado viene firmado por el registro de npm:

npm audit signatures
# 2 packages have verified registry signatures

Para la espera mínima, añada min-release-age al .npmrc. Requiere npm 11.10.0 o posterior, y el valor se mide en días:

# .npmrc, npm 11.10.0 o posterior
min-release-age=1

Queda una diferencia que ningún ajuste de npm cubre. Las dependencias transitivas se pueden importar sin declararlas (punto 3), porque npm coloca en la raíz de node_modules todas las que puede. Eso lo detecta un analizador de dependencias, no una línea de configuración.

8. Qué cuesta cambiar de npm a pnpm

Migrar a pnpm tiene cuatro consecuencias desde el primer día:

  1. El código que importaba paquetes sin declararlos deja de arrancar, con el MODULE_NOT_FOUND del punto 3. Declare cada paquete, y no desactive el aislamiento de pnpm para ocultar el error.
  2. La primera instalación se detiene con la lista de paquetes bloqueados, y alguien tiene que revisarla. Esa revisión es la parte útil del bloqueo, porque obliga a leer por qué un paquete necesita ejecutar código al instalarse.
  3. pnpm-workspace.yaml pasa a formar parte del repositorio, y pnpm lo modifica durante las instalaciones (punto 2). Si aparece una entrada sin que nadie haya añadido una dependencia, compruebe de dónde salió.
  4. La espera mínima retrasa un día cualquier actualización recién publicada, incluida una corrección urgente. Para instalarla antes, añádala a minimumReleaseAgeExclude (punto 5).

Cómo comprobar qué protección tiene un proyecto

La protección depende de la versión que ejecuta la máquina y de su configuración:

  • pnpm 9 y npm 11 sin configurar no bloquean ningún script.
  • pnpm 10 y npm 12 muestran un aviso y continúan.
  • pnpm 11, y npm 12 con strict-allow-scripts=true, detienen la instalación con un error.
  • npm 11 anterior a la 11.16 con ignore-scripts=true bloquea todos los scripts, porque no admite una lista de paquetes autorizados. También bloquea los de los paquetes que sí los necesitan. npm 12 sin configurar bloquea solo los que no están en allowScripts.

Cómo comprobarlo. Consulte la versión que ejecuta la máquina:

npm --version
pnpm --version

Declarar la versión en package.json documenta la intención, pero no la impone:

{ "packageManager": "pnpm@10.11.0" }

En un proyecto con ese campo y sin Corepack activado, pnpm --version sigue devolviendo la versión instalada en la máquina. Esa versión puede ser más antigua que las de la tabla.

Corepack es la utilidad que usa el campo packageManager para instalar esa versión del gestor. Node.js la incluye desde la versión 14.19.0 hasta la 24; a partir de Node.js 25 hay que instalarla con npm install -g corepack13. Para que la versión declarada sea la que se ejecuta, habilite Corepack con corepack enable o instale el gestor como dependencia del propio proyecto. Hasta entonces, el campo es una declaración y no un control.

Las dependencias son una de las diez capas de Cómo proteger una página web, que enlaza el artículo de cada capa.

Referencias

  1. pnpm. Release pnpm 107 de enero de 2025.github.com↩
  2. npm. npm-install-scriptsnpm 12.0.0, 8 de julio de 2026; allowScripts y el bloqueo por defecto.docs.npmjs.com↩
  3. Unit 42, Palo Alto Networks. Shai-Hulud Worm Compromises npm Ecosystem in Supply Chain Attackunit42.paloaltonetworks.com↩
  4. CISA. Widespread Supply Chain Compromise Impacting npm Ecosystem23 de septiembre de 2025.cisa.gov↩
  5. Socket. npm Author Qix Compromised in Major Supply Chain Attacksocket.dev↩
  6. npm. config, ignore-scripts y min-release-agedocumentación de la v12.docs.npmjs.com↩
  7. pnpm. pnpm 11.028 de abril de 2026.pnpm.io↩
  8. pnpm. Settings, Build Settingspnpm.io↩
  9. pnpm. pnpm install, --frozen-lockfilepnpm.io↩
  10. npm. npm-cidocs.npmjs.com↩
  11. pnpm. Settings, minimumReleaseAgepnpm.io↩
  12. GitHub. Our plan for a more secure npm supply chain22 de septiembre de 2025.github.blog↩
  13. Node.js. Corepack, READMEdistribuido con Node.js desde la 14.19.0 hasta la 25.0.0, sin incluirla.github.com↩