Skip to content

[Security] Fix open dependency vulnerabilities - #121

Open
anagperal wants to merge 9 commits into
masterfrom
fix/open-dependency-vulnerabilities
Open

anagperal wants to merge 9 commits into
masterfrom
fix/open-dependency-vulnerabilities

Conversation

@anagperal

@anagperal anagperal commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

ℹ️ Why the gate fails: master still uses the archived i18n tools (d2-i18n-extract/d2-i18n-generate) and has never had @dhis2/cli-app-scripts in its tree. This PR introduces it (a decision already made: maintained-with-vulnerabilities over archived-and-unmaintained), and uuid/request arrive with it, so the gate counts them as introduced by the PR because they genuinely did not exist in master before. Not a stale-baseline artifact: master was rescanned on 2026-10-02 before the last run. It's a real clash between the dependency decision and how the gate compares. The only high left is #735 uuid@3.4.0 (request is medium and does not block), and it needs an approved dismissal before this can merge.

📌 References

📝 Implementation

Four things, in this order:

  1. The alerts open on master after [Security] resolve dependency vulnerabilities #120 that have a published fix.
  2. The i18n tooling moves from the archived @dhis2/d2-i18n-extract and @dhis2/d2-i18n-generate to the maintained @dhis2/cli-app-scripts, with the resolutions its dependency chain needs.
  3. RESOLUTIONS.md is corrected, including two conventions that turned out to be wrong when measured.
  4. The advisories published between 2026-09-28 and 2026-09-30, found when re-auditing after merging master.

1. Alerts open on master

After #120, master had 7 open alerts, and Dependabot and Dependency-Track agreed on all 7. The 6 with a published fix are closed here:

Package Installed Advisory Introduced by Remediation Now
js-yaml 3.15.1 / 4.3.1 GHSA-2883-xcg3-v3hh (high) depcheck (^3.14.1), @eslint/eslintrc (^4.3.0) Re-resolve 3.15.2 / 4.3.2
qs 6.15.3 GHSA-x5fp-wj9c-mxmx, GHSA-4mjr-xmp4-gh2g (medium) @eyeseetea/d2-api and url, through the qs: ^6.15.3 resolution Re-resolve 6.16.0
vitest, @vitest/mocker 3.2.7 GHSA-82fw-gwwq-j7x9 (medium) Direct dev dependency ^3.2.7 → ^4.1.11 4.1.11
  • qs is runtime. @eyeseetea/d2-api only calls qs.stringify(params, { arrayFormat: "repeat" }), and its output is identical between 6.15.3 and 6.16.0 over representative DHIS2 query parameters. The resolution stays at ^6.15.3, since it already admits the fix.
  • vitest 3 → 4. The 3.x line has no fix. The only code change is /// <reference types="vitest/config" /> in vite.config.ts: in vitest 4 only vitest/config adds the test key to vite's config types. Same 13 test files and 137 tests, no snapshot rewritten. ⚠️ yarn.lock now contains a vite@8.2.2 entry that is not installed: vitest 4 declares vite as both a dependency and a peer, and the peer wins, so yarn why vite -R lists only the application's vite.
  • A separate refactor(config) commit removes two implicit any types from vite.config.ts.

elliptic@6.6.1 (low) stays: every published version is affected.

2. i18n tooling: @dhis2/cli-app-scripts

The archived packages receive no fixes when a new advisory lands, and a maintained tool is preferred even when its dependency chain carries findings. @dhis2/cli-app-scripts goes in devDependencies.

  • extract-pot becomes d2-app-scripts i18n extract -p src/ -o i18n/, and localize becomes yarn update-po && d2-app-scripts i18n generate -n dhis2-skeleton-app -p ./i18n/ -o ./src/locales/.
  • Pinned at exactly 12.11.1. 12.11.3 and 12.11.4 add __MANIFEST_APP_TITLE and __MANIFEST_APP_DESCRIPTION to en.pot and to the generated translations whenever there is no d2.config.js. They are meant for DHIS2 app platform manifests, and this project builds its manifest with d2-manifest. With 12.11.1, en.pot keeps the same 39 msgids as master and the generated translations the same 42 keys. The other way to avoid them, a d2.config.js with no entry points, would present this project as an app platform app, which it is not. Same version as metadata-synchronization.
  • What the application displays does not change. i18n.t() was compared for every key in en and es, with the translations generated before and after: 0 differences. The only difference in the generated files is that en.pot gets its msgstr filled with the source string instead of left empty.

The chain's cost, and how it is paid back. Adding the package took yarn npm audit -R from 1 finding to 31 (3 critical, 14 high, 11 moderate, 3 low). Every parent was checked for a release that selects a patched version, and none has one, so the remaining step of the remediation ladder is a scoped resolution per path. Nine were added, and each was verified by calling the code that uses the package, not only by installing or loading it:

Resolution Path it lifts Consumer exercised
@dhis2/cli-app-scripts/vite: ^7.3.6 vite 5.4.21 and its esbuild 0.21.5 yarn localize still extracts and generates the translations; build/start command modules load and import('vite') resolves to 7.3.6; the i18n commands do not load vite
@dhis2/cli-helpers-engine/tar: ^7.5.21 tar 4.4.19 (13 advisories) fetchAndExtract downloads a .tar.gz from a local server with request and extracts it with tar 7
execa@npm:0.7.0/cross-spawn: ^6.0.6 cross-spawn 5.1.0 execa@0.7.0 sync, shellSync, ENOENT hook and async API, and term-size itself
external-editor/tmp: ^0.2.7 tmp 0.0.33 ExternalEditor creates and cleans up its temp file
http-proxy-agent/@tootallnate/once: ^2.0.1 @tootallnate/once 1.1.2 a request through a local proxy
latest-version/package-json: ^7.0.0 got 9.6.0 latestVersion() against a local registry
request/form-data: ^2.5.6 form-data 2.3.3 a multipart upload through request
request/tough-cookie: ^4.1.3 tough-cookie 2.5.0 a cookie jar carried across two request calls
styled-jsx/loader-utils: ^1.4.2 loader-utils 1.2.3 the styled-jsx webpack loader reading its options

Four details worth reading:

  • package-json/got: ^11.8.5 was tried first and breaks its consumer. package-json@6 loads fine and every lookup then fails with The GET method cannot be used with a body: it passes json: true, which got 9 reads as "parse the response" and got 11 as "send a JSON body". Lifting the parent instead, to package-json@7, brings got 11 through a release written for it. Recorded under "Rejected pins".
  • tmp is floored at 0.2.7, not 0.2.6. GHSA-7c78-jf6q-g5cm affects exactly >= 0.2.6, < 0.2.7, so ^0.2.6 would admit a vulnerable release.
  • cross-spawn@5.1.0 is fixable, contrary to what the conventions said. See below.
  • vite is floored at the application's 7.3.6, not at the first patched 6.4.3. vite 6 pulls esbuild@0.25.12, which Dependency-Track reports against GHSA-gv7w-rqvm-qjhr (>= 0.17.0, < 0.28.1). GitHub withdrew that advisory on 2026-06-17, but the alert (#649) still blocked the gate. With the floor at 7.3.6, @dhis2/cli-app-scripts shares the application's vite, the tree only has esbuild@0.28.1, and yarn.lock loses 326 net lines. Neither vite 7.3.6 nor esbuild 0.28.1 has an open advisory.
⚠️ Remaining findings:

elliptic@6.6.1 (above), request@2.88.2 (GHSA-p8p7-x288-28g6, no patched version, deprecated since 2020) and uuid@3.4.0 (GHSA-w5hq-g745-h8pq). uuid cannot be forced to the patched 11.1.1 because request imports the uuid/v4 subpath removed in v7, and it is not reachable: request only calls v4() with no arguments. All three are build tooling and documented in RESOLUTIONS.md.

3. RESOLUTIONS.md

  • Two conventions were wrong, and were measured before being rewritten. The file said a versioned-parent path must use the descriptor, and cannot select outside the parent's declared range. On Yarn 4.15.0, and the same on 4.12.0:

    • glob@npm:7.2.3/minimatch: 3.1.2 binds, while glob@npm:^7.1.3/minimatch: 3.1.2 does nothing, although ^7.1.3 is a real descriptor.
    • execa@npm:0.7.0/cross-spawn: ^6.0.6 selects 6.0.6 although execa@0.7.0 declares ^5.0.1.

    Both rules are rewritten from these measurements, and a new one says a floor must name the highest patched version of the advisories it covers.

  • The file now holds only what a maintainer needs today: active resolutions, rejected pins and known findings without a fix. Removed: "Why the archived i18n packages are still here" (superseded by the switch above), "Future improvements" (moved to tickets), the history of retired pins ("Removed", "Considered and dropped"), and dates in headings. That history stays in git and in [Security] resolve dependency vulnerabilities #120.

  • "Notes for applications copying this baseline" keeps only the couplings an application may not need: vite 7 without ESLint 9, vitest 4, and the i18n switch with its exact 12.11.1.

  • Smaller fixes: the fixtures get their own section, the node-gettext entry names the parent that actually requests it, and the qs entry lists the advisories its floor really covers (GHSA-6rw7, GHSA-q8mj, GHSA-w7fw) and warns that GHSA-4mjr and GHSA-x5fp are only closed because the lockfile resolves 6.16.0.

4. Advisories published after the first audit

Re-audited after merging master and rescanning it: 29 new Dependency-Track alerts, all from advisories published between 2026-09-28 and 2026-09-30, all on packages also present on master, and all with a published fix that clears the 7-day npmMinimalAgeGate.

Package Installed Advisories Introduced by Remediation Now
axios 1.19.0 GHSA-3pq3-5fj3-cg6v, GHSA-542g-h47m-68v8, GHSA-c29m-xwm3-cm6r, GHSA-m8m8-qj5v-23w3, GHSA-mghh-pgcx-3jjj, GHSA-r4gj-5m52-g5wh, GHSA-x97p-jq2g-jp4f (high); GHSA-44g4-m2mj-wpvx, GHSA-4hqw-qxg8-jxx2, GHSA-9fr6-4gfg-395g, GHSA-j8rh-479h-cp32, GHSA-vh66-26gq-q6x8 (medium) @eyeseetea/d2-api, @eyeseetea/feedback-component, @dhis2/cli-app-scripts, through the axios resolution Floor ^1.18.0 → ^1.20.0 1.20.0
brace-expansion 1.1.18 / 2.1.4 / 5.0.9 GHSA-6j4f-fj2g-mc7p, GHSA-qhr7-859c-m2p7 (high); GHSA-q2hr-2g5m-vwhr (medium) minimatch 3, 5 / 7 / 9, and 10 Re-resolve 1.1.21 / 2.1.7 / 5.0.12
undici 6.28.0 GHSA-rfgv-xxqx-mfg5 (high); GHSA-3wwx-pv8p-q78v (medium); GHSA-r53p-7pc4-xj5r (low) node-gyp Re-resolve 6.28.1
markdown-it 14.2.0 GHSA-253c-mchw-3w2r (medium) typedoc Re-resolve 14.3.2
moment 2.30.1 / 2.29.4 GHSA-4p3w-j4w9-5jqw (medium) @dhis2-ui/header-bar, @dhis2/app-shell, @dhis2/d2-i18n; @eyeseetea/d2-ui-components (exactly 2.29.4) Re-resolve; new resolution @eyeseetea/d2-ui-components/moment: ^2.31.0 2.31.0
fast-uri 3.1.7 GHSA-hrr3-gc8f-f4qj (medium) ajv 8 Re-resolve 3.1.8
serialize-javascript 7.1.1 GHSA-gfhx-hw2g-v5hg (low) @rollup/plugin-terser Re-resolve 7.1.2
  • axios is runtime. Verified with a GET, a POST and a redirect against a local server, and with an authenticated D2Api request. The lockfile alone would have reached 1.20.0, but the floor goes up too, since a floor has to name the version that fixes every advisory it covers.
  • moment needs a resolution for one path. @eyeseetea/d2-ui-components requests exactly 2.29.4, and 2.13.0, its latest release, still does. It is runtime: DatePicker was rendered, its calendar opened and a day picked, and formatRowValue and formatDateLong give the same output for ISO strings and Date values. moment 2.31.0 adds a devEngines Node range, which applies only to moment's own development; engines.node is still *.
  • The re-resolved packages were exercised through their consumers: every minimatch major with its brace-expansion, ajv 8 compiling a schema with fast-uri, markdown-it rendering, undici fetch against a local server, and serialize-javascript.
  • The lockfile changes only those nine packages.

Verification

Run on Node 24.13.1 / Yarn 4.15.0:

Check Result
yarn install --immutable ok
yarn check (typecheck, prettier, lint, tests) clean; 14 files, 144 tests (13 and 137 before merging master, which adds the ESLint rule spec)
yarn build completes, including localize, the manifest and the zip step
yarn localize en.pot has the same 39 msgids as master, no __MANIFEST_* keys; t() output identical for every key in en and es
yarn npm audit -R this branch: elliptic, request, uuid
Consumers of every resolution added, and of every package re-resolved in section 4 all pass, as listed above

📹 Screenshots/Screen capture

None

🔥 Notes to the tester

Node 24 is required (.nvmrc): run nvm use first.

  • yarn install, then yarn start: the app loads, a screen that fetches data from DHIS2 works, a date picker opens and accepts a date, and switching the user language to Spanish shows the Spanish strings.
  • yarn localize: completes; git diff i18n/ shows only generation timestamps.
  • yarn check (14 files, 144 tests) and yarn build.
  • yarn npm audit -R: only elliptic, request and uuid.
  • yarn why cross-spawn -R: 6.0.6 under execa@0.7.0 and 7.0.6 elsewhere, no 5.x.
  • yarn why vite -R: 7.3.6 for both @dhis2/cli-app-scripts and the app. The vite@8.2.2 entry in yarn.lock is expected and is not installed.
  • yarn why esbuild -R: only 0.28.1.
  • yarn why moment -R: only 2.31.0.
  • Dependency-Track on this PR: elliptic, request, uuid; uuid is the only high, pending an approved dismissal.

GHSA-2883-xcg3-v3hh (high) affects js-yaml >= 3.0.0 < 3.15.2 and
>= 4.0.0 < 4.3.2. depcheck requests ^3.14.1 and @eslint/eslintrc
requests ^4.3.0, so both ranges already admit the patched releases and
re-resolving the lockfile is the whole fix, with no manifest change.
GHSA-x5fp-wj9c-mxmx affects qs >= 6.14.2 <= 6.15.3 and
GHSA-4mjr-xmp4-gh2g affects qs >= 2.2.5 < 6.16.0 (both medium). The
existing qs: ^6.15.3 resolution already admits 6.16.0, so re-resolving
the lockfile is enough and the resolution is left unchanged.

qs is runtime: @eyeseetea/d2-api only calls
qs.stringify(params, { arrayFormat: "repeat" }), and its output is
identical between 6.15.3 and 6.16.0 for representative DHIS2 query
parameters.
GHSA-82fw-gwwq-j7x9 (medium) affects vitest and @vitest/mocker
>= 2.1.0 < 4.1.11, and the 3.x line has no fix, so the upgrade is a
major. In vitest 4 the test key is only added to vite's UserConfig by
vitest/config, so vite.config.ts now references that entry point;
with the old reference the tests still ran but the config no longer
type-checked.

RESOLUTIONS.md gains a note for applications copying this baseline,
including the vite 8 lockfile entry vitest 4 brings without installing
it.
The config function's { mode } argument and the proxy rewrite's path
parameter were untyped. tsconfig.node.json is not strict, so neither
was reported. Type them as ConfigEnv and string, and rename the
rewrite parameter so it no longer shadows the path import.
@bundlemon

bundlemon Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

BundleMon

No change in files bundle size

Groups updated (1)
Status Path Size Limits
✅ Build Folder
./**/*
918.03KB (+3.54KB +0.39%) +20%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@anagperal
anagperal marked this pull request as draft September 16, 2026 15:11
@dhis2/d2-i18n-extract and @dhis2/d2-i18n-generate are archived and will not
receive fixes. @dhis2/cli-app-scripts does the same job and is maintained, so
it replaces them in devDependencies, and extract-pot and localize now call
d2-app-scripts.

It is pinned at exactly 12.11.1: 12.11.3 and 12.11.4 add __MANIFEST_APP_TITLE
and __MANIFEST_APP_DESCRIPTION to en.pot and to the generated translations
when there is no d2.config.js, and this project builds its manifest with
d2-manifest. en.pot keeps the same 39 msgids; its msgstr is now filled with
the source string, which does not change what i18n.t() returns for any key
in en or es.

The new tool brings an outdated transitive chain (31 findings in
yarn npm audit). The next commit pins it.
Nine scoped resolutions bring yarn npm audit from 31 findings back to
elliptic, request and uuid, none of which has a published fix. No parent in
the chain has a release that selects a patched version, including
@dhis2/cli-app-scripts 12.11.5. Each resolution was verified by calling the
code that uses the package, not only by installing it:

- @dhis2/cli-app-scripts/vite ^6.4.3: build/start modules load, vite 6.4.3
- @dhis2/cli-helpers-engine/tar ^7.5.21: fetchAndExtract downloads and
  extracts a tarball from a local server
- execa@npm:0.7.0/cross-spawn ^6.0.6: execa 0.7 sync, shell, ENOENT and
  async paths, and term-size
- external-editor/tmp ^0.2.7: editor temp file created and cleaned up
- http-proxy-agent/@tootallnate/once ^2.0.1: request through a local proxy
- latest-version/package-json ^7.0.0: latestVersion() on a local registry
- request/form-data ^2.5.6 and request/tough-cookie ^4.1.3: multipart
  upload and cookie jar through request
- styled-jsx/loader-utils ^1.4.2: styled-jsx webpack loader options

package-json/got ^11.8.5 was tried first and breaks package-json 6 ("The GET
method cannot be used with a body"); lifting package-json to 7 brings got 11
through a release written for it. tmp is floored at 0.2.7 because
GHSA-7c78-jf6q-g5cm affects exactly 0.2.6.

RESOLUTIONS.md documents the new entries, request and uuid as known
findings, and the rejected got pin. It also rewrites two conventions that
measurement contradicted on Yarn 4.15.0 and 4.12.0: a versioned-parent key
uses the resolved version, not a descriptor, and it can select a version
outside the parent's declared range. It lists the advisories the qs floor
really covers, and drops the history of retired pins, dates in headings, the
archived-i18n rationale and the future-improvements list, which moved to
tickets.
@anagperal
anagperal marked this pull request as ready for review September 17, 2026 09:40
@adrianq
adrianq requested a review from xurxodev October 2, 2026 08:31

@xurxodev xurxodev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @anagperal

1. Should fix

  • The gate fails on two new high alerts, not only on uuid/request. The run on 134b29d reports New alert instances vs base: high=2 medium=1:

    • #735 uuid@3.4.0, GHSA-w5hq-g745-h8pq. Dependency-Track scores it high and GitHub medium. It is already documented as not reachable and unfixable.
    • #649 GHSA-gv7w-rqvm-qjhr against esbuild. The alert still shows esbuild@0.18.20 because it reuses an older alert from master. The current tree has no esbuild 0.18.x: the SBOM generated the same way as CI only contains esbuild@0.25.12 (pulled in by @dhis2/cli-app-scripts/vite: ^6.4.3) and 0.28.1. GitHub withdrew this advisory, but Dependency-Track still reports it.
    • #734 request@2.88.2 (medium) does not block.

    The description only mentions uuid/request as the reason the gate fails. Please add the esbuild alert as well. Whatever decision unblocks the gate (dismissing the alerts in code scanning, or suppressing them in Dependency-Track so the VEX picks them up) has to cover both alerts, unless esbuild is removed as in recommendation 1 below.

  • Re-run the gate and re-audit before merging. The last run is from 22-09. Today yarn npm audit --recursive on 134b29d reports 31 findings, none of them in the PR description: axios ×12, brace-expansion ×9, undici ×3, fast-uri, markdown-it, moment, serialize-javascript, elliptic, request and uuid. Several are high. Some of them may show up in Dependency-Track as new compared with master, which would keep the gate red even after the two alerts above are dealt with. The audit cadence in RESOLUTIONS.md asks for a re-audit right before requesting review on a change that claims a clean gate.

2. Recommendations non blocking

  1. Option: remove the esbuild alert instead of dismissing it. Changing the resolution in package.json from "@dhis2/cli-app-scripts/vite": "^6.4.3" to "^7.3.6" takes esbuild@0.25.12 out of the tree. Everything then resolves to esbuild@0.28.1, which is outside the advisory range (>= 0.17.0, < 0.28.1). vite also collapses to a single 7.3.6 shared with the application, and yarn.lock loses about 390 net lines (+65 / −454). Checked locally on 134b29d

Re-resolve brace-expansion, undici, markdown-it, fast-uri,
serialize-javascript and moment within their declared ranges. Raise the
axios floor to 1.20.0 and pin d2-ui-components' exact moment 2.29.4
request to ^2.31.0, documented in RESOLUTIONS.md.
vite 6 pulls esbuild 0.25.12, which Dependency-Track still reports
against the withdrawn GHSA-gv7w-rqvm-qjhr. Sharing the application's
vite 7.3.6 leaves only esbuild 0.28.1 in the tree.
@anagperal

Copy link
Copy Markdown
Contributor Author

thanks @anagperal

1. Should fix

  • The gate fails on two new high alerts, not only on uuid/request. The run on 134b29d reports New alert instances vs base: high=2 medium=1:

    • #735 uuid@3.4.0, GHSA-w5hq-g745-h8pq. Dependency-Track scores it high and GitHub medium. It is already documented as not reachable and unfixable.
    • #649 GHSA-gv7w-rqvm-qjhr against esbuild. The alert still shows esbuild@0.18.20 because it reuses an older alert from master. The current tree has no esbuild 0.18.x: the SBOM generated the same way as CI only contains esbuild@0.25.12 (pulled in by @dhis2/cli-app-scripts/vite: ^6.4.3) and 0.28.1. GitHub withdrew this advisory, but Dependency-Track still reports it.
    • #734 request@2.88.2 (medium) does not block.

    The description only mentions uuid/request as the reason the gate fails. Please add the esbuild alert as well. Whatever decision unblocks the gate (dismissing the alerts in code scanning, or suppressing them in Dependency-Track so the VEX picks them up) has to cover both alerts, unless esbuild is removed as in recommendation 1 below.

  • Re-run the gate and re-audit before merging. The last run is from 22-09. Today yarn npm audit --recursive on 134b29d reports 31 findings, none of them in the PR description: axios ×12, brace-expansion ×9, undici ×3, fast-uri, markdown-it, moment, serialize-javascript, elliptic, request and uuid. Several are high. Some of them may show up in Dependency-Track as new compared with master, which would keep the gate red even after the two alerts above are dealt with. The audit cadence in RESOLUTIONS.md asks for a re-audit right before requesting review on a change that claims a clean gate.

2. Recommendations non blocking

  1. Option: remove the esbuild alert instead of dismissing it. Changing the resolution in package.json from "@dhis2/cli-app-scripts/vite": "^6.4.3" to "^7.3.6" takes esbuild@0.25.12 out of the tree. Everything then resolves to esbuild@0.28.1, which is outside the advisory range (>= 0.17.0, < 0.28.1). vite also collapses to a single 7.3.6 shared with the application, and yarn.lock loses about 390 net lines (+65 / −454). Checked locally on 134b29d

Thanks @xurxodev, all three points are addressed!

  • esbuild: applied your recommendation in 7c8aa04 (vite: ^7.3.6), so #649 is gone without a dismissal.
  • Re-audit: merged master, rescanned it, and fixed all the new advisories in cff6ef6 (section 4 of the description).
  • Gate: now only blocked by #735 uuid, which has no fix. Dismissal process proposed to @cgbautista
  • PR description updated.

@MiquelAdell
MiquelAdell requested a review from xurxodev October 5, 2026 08:44
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.

2 participants