Skip to content

chore(deps): npm audit en verde con subidas y excepciones que caducan - #358

Merged
aacienfuegos merged 3 commits into
developfrom
chore/npm-audit-advisories-357
Oct 13, 2026
Merged

aacienfuegos merged 3 commits into
developfrom
chore/npm-audit-advisories-357

Conversation

@cienfuegos-agentes

@cienfuegos-agentes cienfuegos-agentes Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Closes #357

El gate npm audit --audit-level=high falla en CI, staging y producción por advisories publicadas después del último merge a develop, y con él se saltan Build y los smoke tests de todos los PR. Este PR sube lo que tiene versión corregida, cambia el gate por un script con excepciones que caducan y pide la revisión del propietario en los cambios del gate.

Subidas

Paquete Versión Advisory
next 16.3.4 → 16.3.8 GHSA-vcvr-r3jv-pc5j (critical)
eslint-config-next 16.3.0 → 16.3.8 va con next
sharp (override next.sharp) 0.35.4 → 0.35.5 GHSA-wq5f-xc86-pv6w
brace-expansion (override) 5.0.9 → 5.0.12 GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p
proxy-addr 2.0.7 → 2.0.8 GHSA-jqcg-44mw-7w3h (critical)
@modelcontextprotocol/sdk 1.30.0 → 1.32.1 GHSA-6qxp-vccf-f47h
undici 7.29.0 → 7.30.0 GHSA-rfgv-xxqx-mfg5, GHSA-w293-vg96-wgc3
source-map-js 1.2.1 → 1.2.2 GHSA-68fv-2mgg-jv7q
shell-quote 1.10.0 → 1.12.0 GHSA-pqg4-j6r4-53mv (critical)
compression 1.8.1 → 1.8.2 GHSA-vc2v-76pw-4v95

En el lockfile solo cambian las versiones de estos paquetes, @next/* y @img/sharp-*. Las 1600 claves de packages son las mismas que en develop, no entra ni sale ningún paquete y apps/android/package.json no cambia. Sin npm audit fix --force ni npx expo install --fix (#304); ninguna de las subidas está en bundledNativeModules.json de Expo.

npm audit (npm 11.19.0) pasa de 45 vulnerabilidades (3 critical, 33 high, 9 moderate) a 35 (0 critical, 26 high, 9 moderate). Las 26 high cuelgan de las dos advisories de abajo.

Excepciones, hasta el 2027-01-04

Ninguna de las dos tiene alternativa: las últimas versiones de las herramientas que las traen siguen dependiendo de ellas.

Gate

npm audit no admite excepciones. scripts/npm-audit.mjs (Node, sin dependencias) ejecuta npm audit --json y lee scripts/audit-exceptions.json:

  • Bloquea toda advisory high o critical que no tenga una excepción vigente para ese GHSA y ese paquete. La misma advisory en otro paquete vuelve a bloquear.
  • Falla con una excepción caducada, aunque la advisory ya no aparezca, y con una excepción cuya fecha pase de hoy + 90 días.
  • Falla cerrado si no puede auditar, si el informe no tiene el formato que conoce o si los recuentos de metadata no cuadran con las entradas de vulnerabilities.
  • Antes de auditar comprueba el lockfile: rechaza npm-shrinkwrap.json, los alias y cualquier entrada que no venga del registry de npm con su nombre y su versión, exige version semver y name válido de npm, y solo admite links hacia los workspaces de package.json.

El paso "Auditoría de seguridad npm" cambia en los tres workflows a la vez: si solo cambiara ci.yml, el primer push a develop tras el merge fallaría en staging. Además se mueve justo detrás de "Setup Node.js", antes de que el job ejecute código del repo. npm audit solo necesita el lockfile, así que el resultado no cambia.

CODEOWNERS

Para dejar el gate en verde no hace falta tocar las excepciones: basta con cambiar el script, quitar el paso del workflow o añadir un .npmrc, que cambia lo que audita npm. .github/CODEOWNERS pone a @aacienfuegos en /.github/ (workflows y el propio CODEOWNERS), en los dos ficheros de scripts/ y en cualquier .npmrc del repo. El fichero solo hace que GitHub pida la revisión. La vuelve obligatoria el ruleset de abajo.

Después del merge (Andrés)

Ruleset en Settings > Rules > Rulesets > New branch ruleset. Va después del merge porque GitHub lee el CODEOWNERS de la rama base.

  • Nombre "Gate de seguridad: revisión de Andrés", Enforcement status "Active".
  • Bypass list: "Repository admin" en "For pull requests only", para los PR de release develop → main, que abres tú y no puedes aprobarte.
  • Target branches: develop y main.
  • "Require a pull request before merging" con 0 aprobaciones, "Require review from Code Owners" y "Dismiss stale pull request approvals when new commits are pushed". Las reglas que GitHub marca por defecto pueden quedarse.

Con 0 aprobaciones, un PR que no toca esas rutas sigue sin necesitar aprobación. En main no pide nada hasta el primer release que lleve el CODEOWNERS. La comprobación del ruleset, por configuración y por el efecto, va en otro issue.

Verificación

Type check (web y Android), tests, build y smoke tests: no se han ejecutado en local por falta de espacio en disco para instalar las dependencias. Los ha ejecutado el CI de este PR, run 37950677527 sobre 5807ef5, con conclusión success y todos los pasos en success:

Auditoría de seguridad npm   node scripts/npm-audit.mjs → braces y node-forge exceptuadas hasta 2027-01-04
                             npm audit: 0 critical, 30 high
Type check                   npx tsc --noEmit (apps/web)
Type check Android           npx tsc --noEmit (apps/android)
Tests                        Test Files 29 passed (29) · Tests 371 passed (371)
Build                        ✓ Compiled successfully in 13.0s
Migrar base de datos         All migrations have been successfully applied.
Smoke test del entrypoint    ✅ Seed completado

El CI (Node 26.11.1, npm 11.20.0, 15:19 UTC) cuenta 30 entradas high y en local salen 26: con npm 11.19.0 y con 11.20.0, sobre el mismo lockfile, a las 19:02 UTC. Las dos auditorías dan exit 0 y solo encuentran como advisories high las de braces y node-forge. Con el recuento estricto, eso implica que en el CI las 30 entradas llevaban a una de esas dos. No sé qué cuatro entradas sobran en el CI, porque el script solo imprime el recuento. Que la base de advisories cambiara entre una hora y otra es una deducción, no lo he comprobado.

sharp 0.35.5 carga su binario nativo (instalado aparte; la integrity de sharp, @img/sharp-linux-x64 y @img/sharp-libvips-linux-x64 es la misma que en el lockfile de la rama):

sharp 0.35.5 vips 8.18.7 rsvg 2.63.2

Versiones en el lockfile (jq -r '.packages["node_modules/<paquete>"].version' package-lock.json):

next 16.3.8
eslint-config-next 16.3.8
sharp 0.35.5
brace-expansion 5.0.12
proxy-addr 2.0.8
@modelcontextprotocol/sdk 1.32.1
undici 7.30.0
source-map-js 1.2.2
shell-quote 1.12.0
compression 1.8.2

Gate en la rama y controles negativos. Cada control se ejecuta en local y sin commitear. Las excepciones se modifican sobre una copia y se restauran después; los lockfiles de develop salen de git show origin/develop:<fichero> a un directorio aparte, con los cuatro package.json, y el script se ejecuta desde ahí. Todos dan exit 1 y cada uno por su motivo, salvo el de la rama, que da exit 0. El 5 falla por las advisories de develop y no por el formato ni por el lockfile: el recuento estricto y la comprobación del lockfile no dan falsos positivos con datos reales.

Salida de los 17 casos
### Rama: gate en verde
$ node scripts/npm-audit.mjs
braces GHSA-vfj7-8cjw-p6xm (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=3.0.3) y el mantenedor rechaza el CVE (micromatch/braces#70). Llega por metro, Expo, tailwindcss 3, shadcn, ts-morph y patch-package; los patrones vienen de la configuración del repo, nunca de un usuario.
node-forge GHSA-86w9-cpqp-85rv (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=1.4.0); el fix está en digitalbazaar/forge#1152, sin publicar. Solo lo trae @expo/cli, herramienta de desarrollo de Android; no está en la imagen web y apps/android no usa code signing de Expo.
npm audit: 0 critical, 26 high
exit=0

### 1. Sin la excepción de node-forge
$ node scripts/npm-audit.mjs
braces GHSA-vfj7-8cjw-p6xm (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=3.0.3) y el mantenedor rechaza el CVE (micromatch/braces#70). Llega por metro, Expo, tailwindcss 3, shadcn, ts-morph y patch-package; los patrones vienen de la configuración del repo, nunca de un usuario.
npm audit: 0 critical, 26 high
bloquean el gate:
  node-forge GHSA-86w9-cpqp-85rv (high): node-forge RSA PKCS#1 v1.5 signature verification accepts extra nested DigestAlgorithm elements
exit=1

### 2. expires de braces en el día de ayer (2026-10-05)
$ node scripts/npm-audit.mjs
node-forge GHSA-86w9-cpqp-85rv (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=1.4.0); el fix está en digitalbazaar/forge#1152, sin publicar. Solo lo trae @expo/cli, herramienta de desarrollo de Android; no está en la imagen web y apps/android no usa code signing de Expo.
npm audit: 0 critical, 26 high
bloquean el gate:
  braces GHSA-vfj7-8cjw-p6xm (high): braces vulnerable to stack-exhaustion denial of service through deeply nested patterns
  excepción caducada el 2026-10-05: GHSA-vfj7-8cjw-p6xm en braces. Revisa si hay versión corregida y renuévala o quítala
exit=1

### 3. expires de node-forge en 2027-06-01
$ node scripts/npm-audit.mjs
excepciones inválidas
  GHSA-86w9-cpqp-85rv en node-forge: expires 2027-06-01 pasa del tope de 90 días (2027-01-04)
exit=1

### 4. package de la excepción de braces cambiado a micromatch
$ node scripts/npm-audit.mjs
node-forge GHSA-86w9-cpqp-85rv (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=1.4.0); el fix está en digitalbazaar/forge#1152, sin publicar. Solo lo trae @expo/cli, herramienta de desarrollo de Android; no está en la imagen web y apps/android no usa code signing de Expo.
aviso: la excepción de GHSA-vfj7-8cjw-p6xm en micromatch ya no tiene advisory high o critical; se puede quitar
npm audit: 0 critical, 26 high
bloquean el gate:
  braces GHSA-vfj7-8cjw-p6xm (high): braces vulnerable to stack-exhaustion denial of service through deeply nested patterns
exit=1

### 5. Lockfile de develop (origin/develop, e85fa14)
$ node <rama>/scripts/npm-audit.mjs
braces GHSA-vfj7-8cjw-p6xm (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=3.0.3) y el mantenedor rechaza el CVE (micromatch/braces#70). Llega por metro, Expo, tailwindcss 3, shadcn, ts-morph y patch-package; los patrones vienen de la configuración del repo, nunca de un usuario.
node-forge GHSA-86w9-cpqp-85rv (high) exceptuada hasta 2027-01-04: Sin versión corregida (<=1.4.0); el fix está en digitalbazaar/forge#1152, sin publicar. Solo lo trae @expo/cli, herramienta de desarrollo de Android; no está en la imagen web y apps/android no usa code signing de Expo.
npm audit: 3 critical, 33 high
bloquean el gate:
  @modelcontextprotocol/sdk GHSA-6qxp-vccf-f47h (high): MCP TypeScript SDK: OAuth client could send credentials to an authorization server chosen by the MCP server
  brace-expansion GHSA-qhr7-859c-m2p7 (high): brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion
  brace-expansion GHSA-6j4f-fj2g-mc7p (high): brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion
  compression GHSA-vc2v-76pw-4v95 (high): compression vulnerable to Denial of Service via memory leak on premature response close
  next GHSA-vcvr-r3jv-pc5j (critical): Next.js: Remote Code Execution in next/og ImageResponse
  proxy-addr GHSA-jqcg-44mw-7w3h (critical): proxy-addr vulnerable to IP spoofing via IPv4-mapped IPv6 trust subnet
  sharp GHSA-wq5f-xc86-pv6w (high): sharp : Vulnerability in librsvg dependency CVE-2026-96889
  shell-quote GHSA-pqg4-j6r4-53mv (critical): shell-quote: `quote()` command injection via a line terminator in a token after a `{ comment }` token
  source-map-js GHSA-68fv-2mgg-jv7q (high): source-map-js allows event-loop denial of service through indexed source-map section offsets
  undici GHSA-rfgv-xxqx-mfg5 (high): undici vulnerable to Denial of Service via unrequested WebSocket subprotocol
  undici GHSA-w293-vg96-wgc3 (high): undici vulnerable to TLS certificate validation bypass via dropped connect options in BalancedPool
exit=1

### 6. Sin acceso al registry
$ npm_config_registry=http://127.0.0.1:9 node scripts/npm-audit.mjs
no se pudo auditar
  {"summary":"","detail":""}
exit=1

### 7a. --report a.json
$ node scripts/npm-audit.mjs --report reports/a.json
formato inesperado
  regla (b): micromatch (high) no llega a ninguna advisory high o critical
exit=1

### 7b. --report b.json
$ node scripts/npm-audit.mjs --report reports/b.json
formato inesperado
  braces: severity "severe" desconocida
exit=1

### 7c. --report c.json
$ node scripts/npm-audit.mjs --report reports/c.json
formato inesperado
  braces: url "https://github.com/advisories/1240992" no termina en un GHSA
exit=1

### 7d. --report d.json
$ node scripts/npm-audit.mjs --report reports/d.json
formato inesperado
  regla (a): metadata.vulnerabilities.high = 2, entradas high = 1
exit=1

### 7e. --report e.json
$ node scripts/npm-audit.mjs --report reports/e.json
formato inesperado
  regla (b): foo (high) no llega a ninguna advisory high o critical
exit=1

### 7f. --report f.json
$ node scripts/npm-audit.mjs --report reports/f.json
formato inesperado
  regla (b): foo (high) no llega a ninguna advisory high o critical
  regla (b): bar (high) no llega a ninguna advisory high o critical
exit=1

### 8. Lockfile de develop con version de node_modules/shell-quote a 1.12.0 (resolved e integrity sin tocar)
$ jq -c '.packages["node_modules/shell-quote"]' package-lock.json; node <rama>/scripts/npm-audit.mjs
{"version":"1.12.0","resolved":"https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz","integrity":"sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==","license":"MIT","engines":{"node":">= 0.4"},"funding":{"url":"https://github.com/sponsors/ljharb"}}
lockfile inesperado
  node_modules/shell-quote: resolved https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz no es https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz
exit=1

### 9. Directorio sin package-lock.json
$ ls; node <rama>/scripts/npm-audit.mjs
package.json
lockfile inesperado
  package-lock.json no existe o no es JSON
exit=1

### 10. Control de npm-shrinkwrap.json
exit 1, lockfile inesperado

### 11. Control de alias
exit 1, lockfile inesperado

Los seis informes escritos a mano (casos 7a a 7f)

a.json

{"auditReportVersion":2,"vulnerabilities":{"micromatch":{"name":"micromatch","severity":"high","via":["braces"]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":1,"critical":0,"total":1}}}

b.json

{"auditReportVersion":2,"vulnerabilities":{"braces":{"name":"braces","severity":"high","via":[{"source":1240992,"name":"braces","dependency":"braces","title":"braces vulnerable to stack-exhaustion denial of service through deeply nested patterns","url":"https://github.com/advisories/GHSA-vfj7-8cjw-p6xm","severity":"severe","cwe":["CWE-674"],"cvss":{},"range":"<=3.0.3"}]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":1,"critical":0,"total":1}}}

c.json

{"auditReportVersion":2,"vulnerabilities":{"braces":{"name":"braces","severity":"high","via":[{"source":1240992,"name":"braces","dependency":"braces","title":"braces vulnerable to stack-exhaustion denial of service through deeply nested patterns","url":"https://github.com/advisories/1240992","severity":"high","cwe":["CWE-674"],"cvss":{},"range":"<=3.0.3"}]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":1,"critical":0,"total":1}}}

d.json

{"auditReportVersion":2,"vulnerabilities":{"braces":{"name":"braces","severity":"high","via":[{"source":1240992,"name":"braces","dependency":"braces","title":"braces vulnerable to stack-exhaustion denial of service through deeply nested patterns","url":"https://github.com/advisories/GHSA-vfj7-8cjw-p6xm","severity":"high","cwe":["CWE-674"],"cvss":{},"range":"<=3.0.3"}]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":2,"critical":0,"total":2}}}

e.json

{"auditReportVersion":2,"vulnerabilities":{"braces":{"name":"braces","severity":"high","via":[{"source":1240992,"name":"braces","dependency":"braces","title":"braces vulnerable to stack-exhaustion denial of service through deeply nested patterns","url":"https://github.com/advisories/GHSA-vfj7-8cjw-p6xm","severity":"high","cwe":["CWE-674"],"cvss":{},"range":"<=3.0.3"}]},"foo":{"name":"foo","severity":"high","via":["bar"]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":2,"critical":0,"total":2}}}

f.json

{"auditReportVersion":2,"vulnerabilities":{"braces":{"name":"braces","severity":"high","via":[{"source":1240992,"name":"braces","dependency":"braces","title":"braces vulnerable to stack-exhaustion denial of service through deeply nested patterns","url":"https://github.com/advisories/GHSA-vfj7-8cjw-p6xm","severity":"high","cwe":["CWE-674"],"cvss":{},"range":"<=3.0.3"}]},"foo":{"name":"foo","severity":"high","via":["bar"]},"bar":{"name":"bar","severity":"high","via":["foo"]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":3,"critical":0,"total":3}}}

Orden de los pasos en los tres workflows:

ci.yml: Checkout → Setup Node.js → Auditoría de seguridad npm → Instalar dependencias
deploy-staging.yml: Checkout → Setup Node.js → Auditoría de seguridad npm → Instalar dependencias
deploy-production.yml: Checkout → Setup Node.js → Auditoría de seguridad npm → Instalar dependencias

En el log del run del PR, "Auditoría de seguridad npm" es el paso 5, justo detrás de "Setup Node.js" (4) y antes de "Instalar dependencias" (6):

3 Checkout · 4 Setup Node.js · 5 Auditoría de seguridad npm · 6 Instalar dependencias · 7 Generar cliente Prisma · 8 Type check · 9 Type check Android · 10 Tests · 11 Build · 12 Migrar base de datos (smoke test) · 13 Smoke test del entrypoint real

CODEOWNERS (configuración; el efecto lo comprueba el issue del ruleset):

$ gh api "repos/aacienfuegos/tripplanner/codeowners/errors?ref=chore/npm-audit-advisories-357"
{"errors":[]}

Ficheros que cambia el PR (git diff --stat origin/develop):

 .github/CODEOWNERS                      |   5 +
 .github/workflows/ci.yml                |   6 +-
 .github/workflows/deploy-production.yml |   6 +-
 .github/workflows/deploy-staging.yml    |   6 +-
 apps/web/package.json                   |   4 +-
 package-lock.json                       | 383 ++++++++++++++++----------------
 package.json                            |   4 +-
 scripts/audit-exceptions.json           |  14 ++
 scripts/npm-audit.mjs                   | 151 +++++++++++++
 9 files changed, 379 insertions(+), 200 deletions(-)

🤖 Generated with Claude Code

cienfuegos-agentes Bot and others added 3 commits October 9, 2026 17:14
El gate de npm audit fallaba en CI, staging y producción por advisories
nuevas (next critical, proxy-addr, shell-quote y otras), y con él se
saltaban Build y los smoke tests de todos los PR.

- Sube next y eslint-config-next a 16.3.8, sharp a 0.35.5 y
  brace-expansion a 5.0.12 (overrides), y las transitivas proxy-addr,
  shell-quote, undici, @modelcontextprotocol/sdk, source-map-js y
  compression. Solo cambian versiones: ningún paquete entra ni sale.
- braces y node-forge no tienen versión corregida ni alternativa.
  npm audit no admite excepciones, así que el gate pasa a
  scripts/npm-audit.mjs, que las lee de scripts/audit-exceptions.json
  por GHSA y paquete, con caducidad de 90 días como máximo, y falla
  cerrado si no puede auditar o no reconoce el lockfile o el informe.
- La auditoría corre justo después de setup-node, antes de que el job
  ejecute código del repo.
- CODEOWNERS pide la revisión de Andrés en los cambios del gate, sus
  excepciones y la configuración de npm.

Closes #357

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
El resolved esperado de cada entrada se construye con su version y el
name de su clave, así que la comparación solo vale si los dos tienen la
forma que publica npm. Ahora el gate exige version semver y name válido
de npm, y que el resolved no cambie al normalizarse como URL; si no,
falla con "lockfile inesperado".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Las entradas link del lockfile salían de la comprobación sin mirar a
dónde apuntaban. Ahora un link solo vale si su resolved es un workspace
de package.json#workspaces, sin segmentos node_modules ni .., y si la
entrada de ese workspace en el lockfile tiene el mismo name que la clave
del link; si no, falla con "lockfile inesperado". Los workspaces se leen
de package.json porque packages[""] viene en el mismo lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aacienfuegos
aacienfuegos merged commit a496bed into develop Oct 13, 2026
1 check passed
@aacienfuegos
aacienfuegos deleted the chore/npm-audit-advisories-357 branch October 13, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant