[SECURITY] resolve dependency vulnerabilities - #413
Open
anagperal wants to merge 7 commits into
Open
Conversation
…tions - Re-resolve stale transitives within the ranges their parents already request: handlebars 4.7.9, @babel/traverse 7.29.8, pbkdf2, minimist, sha.js, cipher-base, postcss, semver, json5, picomatch, tar and others. - Convert the inherited exact-version pins to floors: axios ^1.18.0, lodash/lodash-es ^4.18.0, form-data ^4.0.6, minimatch ^3.1.4, brace-expansion ^1.1.17, qs ^6.15.3, jszip ^3.8.0. An exact version cannot select a patch, so each had stopped doing its job. They cannot simply be dropped: @eyeseetea/d2-api requests exact axios@1.6.4, lodash@4.17.21 and qs@6.9.7, so removing a constraint resolves downwards. - Add scoped resolutions react-linkify/linkify-it ^5.0.2 and i18next-conv/node-gettext ^3.0.1, where the parent ranges cannot reach the fix and the parents are themselves pinned by their consumers. - Migrate vite ^4.2.0 to ^7.3.6 and vitest ^0.32.2 to ^3.2.7, with @vitejs/plugin-react ^5.1.0 and vite-plugin-checker ^0.11.0, pulling esbuild to 0.28.1. vite 7 is required because 6.4.3 caps esbuild at ^0.25.0. Stopping at vite-plugin-checker 0.11.0 keeps eslint 8. - Remove babel-core@6.26.3 and postcss-rtl@1.7.3: both unused, and the sole paths to babel-traverse@6.26.0 and postcss@6.0.23. - Remove the path-to-regexp resolution, which matched no descriptor. Add RESOLUTIONS.md, documenting every resolution with why/fixes/drop-when, the inherited pins whose rationale was never recorded, and the reason the archived @dhis2/d2-i18n-* pair is not yet replaced.
- Re-resolve within the ranges parents already request: micromatch 4.0.8, browserslist 4.28.7, word-wrap 1.2.5, path-parse 1.0.7, bn.js 4.12.5, @sideway/formula 3.0.1, joi 17.13.4, yaml 1.10.3, ajv 6.15.0, @tootallnate/once 2.0.1, @babel/helpers and @babel/runtime-corejs3 7.29.7. - Bump react-router-dom to 6.30.4, the patched release on the 6.x line. - Open @babel/core and @babel/preset-typescript from exact 7.16.7 to ranges. babel-preset-react-app and eslint-config-react-app already request ^7.16.0, so this collapses the two babel versions in the tree onto one. Record in RESOLUTIONS.md the two findings with no remediation available: elliptic, whose advisory covers every published version, and a withdrawn eslint advisory that should be dismissed rather than fixed.
The entry cited GHSA-g8qh-9h27-crr2, which does not exist. The advisory is GHSA-g974-hxvm-x689 — same package, same affected range (<= 3.0.0), same absence of a recorded patched version, so the reasoning in the entry is unchanged.
The three remaining react-router advisories had no entry in RESOLUTIONS.md, so the reasoning lived only in the PR description and would have been lost once the PR closed. Records that the 6.x line has no fix — two advisories are patched in 7.18.0 and the third covers 6.30.4, the final 6.x release — that the bump to 6.30.4 still cleared a fourth advisory, and that anyone picking up the migration should go straight to v7 rather than stopping on 6.x.
anagperal
marked this pull request as ready for review
August 5, 2026 07:37
Each was tested by removing it, re-installing and comparing the resolved versions. None of the four changes what is installed in a way that matters, and all remaining versions are outside every advisory that affects them. form-data and jszip were inert. Every parent range already reaches the patched release - form-data stays at 4.0.6 and jszip at 3.10.1 with or without the entry. The original reasoning noted that d2-api requests form-data at ^4.0.0 'which admits affected 4.0.x releases', which is true but incomplete: ^4.0.0 admits the patched 4.0.6 as well. minimatch and brace-expansion met the drop-when condition the minimatch entry itself recorded. The seven parents that request minimatch declare ^3.0.4, which already admits the patched 3.1.5, and brace-expansion only existed on the 1.x line because minimatch was held at 3.x. Removing both gives minimatch 10.2.6 alongside 3.1.5, and brace-expansion 5.0.9 alongside 1.1.18 - all four patched. Removing the minimatch entry also lets glob@13.0.6 have the ^10.2.2 it declares, instead of being pulled down seven majors. Records why that downgrade never broke glob@13 - its default entry is a bundle with minimatch inlined - so the next audit does not re-investigate it, and notes the compatibility checks behind the linkify-it entry. Also restates the retirement test as 'does a resolved version move' rather than 'is the lockfile byte-identical'. The latter proves a constraint did nothing, but the reverse does not hold. Verified: yarn install --immutable, tsc --noEmit, lint, 137 tests, localize, build.
Eight resolutions inherited from the copied February 2026 security commit were written as exact versions. By their own descriptions several are security floors, which is the shape that cannot receive a patch and eventually becomes the finding it was added to prevent - the same decay that had already happened to axios and qs in this repository. None of them was inside a live advisory range, checked against the published advisory database, so this is not a remediation. It removes a decay path before it opens. @babel/runtime 7.26.10 -> ^7.26.10 resolves 7.29.7 glob-parent 5.1.2 -> ^5.1.2 resolves 5.1.2 moment 2.29.4 -> ^2.29.4 resolves 2.30.1 nanoid 3.3.8 -> ^3.3.8 resolves 3.3.17 node-fetch 2.6.7 -> ^2.6.7 resolves 2.7.0 diff 5.2.2 -> ^5.2.2 resolves 5.2.2 debug 4.3.4 -> ^4.3.4 resolves 4.4.3 ua-parser-js 0.7.24 -> ^0.7.24 resolves 0.7.41 A caret keeps every consumer on the major the exact version already forced it onto, so no package changes line. All eight resulting versions are outside every advisory affecting them. i18next is deliberately left exact: it sits far below the current release line and a resolution on it can break application startup, so it changes only with an app-startup check behind it. The direct moment dependency was reopened to the same range; it still said 2.29.4 while the resolution allowed more, which is the same contradiction axios and lodash had. Also records that nanoid was holding postcss below the ^3.3.16 it declares, and node-fetch below the ^2.7.0 one consumer declares - a constraint holding a consumer beneath its own parent's range is worth treating as a signal in itself. Verified: install --immutable, tsc --noEmit, lint, 137 tests, localize with no content change, full build, and moment 2.30.1 formatting with a Spanish locale.
.yarnrc.yml declared only nodeLinker, so three of the four supply-chain
settings fell back to yarn's defaults, which are looser than what the
other repositories in this family run:
before (effective) after
npmMinimalAgeGate 0 7d
enableScripts true false
enableHardenedMode null true
checksumBehavior throw throw (already matched)
None of these was a decision - they were simply never declared - so this
repository was the most permissive of the three while carrying the same
kind of dependency risk.
Alignment costs nothing here: the lockfile comes back byte-identical, and
install, typecheck, lint, tests, localize and the full build all pass
unchanged.
enableScripts: false is the one worth knowing about. Yarn now reports
YN0004 for esbuild and three core-js variants. Neither breaks - esbuild
ships its binary as a platform-specific optional package rather than
fetching it in a postinstall, verified by running a transform through it,
and core-js's script only prints a funding message. RESOLUTIONS.md now
carries that note, along with the warning that the age gate is what makes
a reachable fix look unreachable.
Verified: install --immutable, tsc --noEmit, lint, 137 tests, localize,
full build, and an esbuild transform executed directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📌 References
OWASP Dependency-Track (yarn4); the other tools in that view report different scopes.feature/test_dependency_track(Add dependency track checks #405), notdevelopment, because Add dependency track checks #405 is what forwardsDTRACK_API_KEYto the workflow. Without it the scan jobs are skipped and the workflow still goes green, sodevelopmenthas no analyses at all and there is nothing to measure against.📝 Implementation
This is a dedicated remediation PR, so the bar applied was fix everything that has a published fix, not introduce nothing new.
Resolves every critical and high vulnerability finding on this branch, as reported by Dependency-Track — the tool the CI gate reads, and the only one quoted here.
yarn npm auditscores several of these packages differently, so the two are never mixed.What remains is medium-severity only, and each one is accounted for below. Check the current alerts rather than a figure quoted here: a count in a PR description is only true on the day it is written, and the scanner is the authority.
Adds
RESOLUTIONS.md, which this repository did not have.Fixed without adding any pin
The lockfile was holding these below the patched release even though every parent already accepted it. A refresh was enough — no maintenance debt added. Most of the criticals were a stale lockfile, not an unfixable path.
handlebars@dhis2/d2-i18n-generatewants^4.0.11@babel/traverse^7.7.0,^7.13.0,^7.16.7pbkdf2parse-asn1wants^3.0.3minimist^1.1.0–^1.2.5sha.js^2.4.0/^2.4.8cipher-base^1.0.0–^1.0.3@babel/plugin-transform-modules-systemjs@babel/corepostcss,semver,json5,picomatch,tar,flatted,ansi-regex,braces,cross-spawn,decode-uri-component,follow-redirects,js-yaml,ws,ip-addressDecayed pins bumped to
^rangesThis repository carried resolutions copied from the skeleton's February 2026 security commit. Being exact versions, no patch could ever be selected, and they had decayed into the findings they were added to fix.
axios1.13.5^1.18.0lodash,lodash-es4.17.23^4.18.0qs6.14.2^6.15.3axiosis the clearest case for the whole change: pinned at an exact version to fix a vulnerability, it had accumulated 10 open high-severity advisories because no patch could ever land.lodash: 4.17.23was the last vulnerable version of lodash. With^ranges both resolve to a patched release on their own.@eyeseetea/d2-api@1.21.0requests exactaxios@1.6.4,lodash@4.17.21andqs@6.9.7, and@eyeseetea/d2-ui-components@2.12.0requests exactlodash@4.17.21— so removing a constraint resolves downwards into the affected range. They are required as floors, which is why they are ranges rather than exact versions.The direct
axiosandlodashdeclarations independencieswere updated to the same floors; they still said1.13.5and4.17.23, contradicting the resolutions.Four more constraints that turned out not to be needed at all
form-data,jszip,minimatchandbrace-expansionwere bumped from exact versions to ranges earlier in this PR, on the same reasoning as the three above. Re-testing them afterwards — remove, re-install, compare the resolved versions — showed that none of them was doing anything the parents were not already doing. All four have been retired.form-data: ^4.0.6^4.0.0(×2),^4.0.6jszip: ^3.8.0^3.2.2,3.10.1minimatch: ^3.1.4^3.0.4(×7) — already admits the patched 3.1.5brace-expansion: ^1.1.17minimatch@3.xat^1.1.7— already admits 1.1.18The
minimatchpair is the interesting one, and it is a correction to the skeleton's approach rather than a copy of it. Its own drop-when condition — "every consumer resolves to a patched minimatch on its own line without it" — turned out to be satisfied already: the seven parents that requestminimatchdeclare^3.0.4, which admits the patched 3.1.5 unaided, andbrace-expansiononly existed on the 1.x line becauseminimatchwas being held at 3.x. Removing both leavesminimatchat 10.2.6 and 3.1.5, andbrace-expansionat 1.1.18 and 5.0.9 — all four outside every advisory affecting them.Removing the
minimatchconstraint also letsglob@13.0.6have the^10.2.2it declares, instead of being pulled down seven majors.The test matters as much as the result. Removing
minimatchandbrace-expansiondoes change the lockfile — new lines appear — so a byte-identical comparison would have reported them as load-bearing and kept them. What settles it is that no resolved version ends up anywhere an advisory reaches.Scoped pins added
react-linkify/linkify-it: ^5.0.2react-linkify@1.0.0-alphawants^2.0.3; the fix is on 5.x. The parent is requested at an exact version by@eyeseetea/d2-ui-components, so it cannot be moved either. Same approach as #120.react-linkifyis unmaintained and written against the linkify-it 2 API, so compatibility was checked rather than assumed — see the tester notesi18next-conv/node-gettext: ^3.0.1i18next-conv@6.1.1wants^2.0.0. The advisory records no patched version, but its affected range is<= 3.0.0and 3.0.1 is published and outside it — the dead end came from advisory metadata, not from the registryToolchain migration — vite and vitest
vitest 0.32.4carried the last critical (GHSA-5xrq-8626-4rwp), patched only in 3.2.6+, which needs vite 5+. There was no smaller fix.vite^4.2.0(4.5.14)^7.3.6vitest^0.32.2(0.32.4)^3.2.7@vitejs/plugin-react^3.1.0^5.1.0(5.2.0)vite-plugin-checker^0.6.2^0.11.0esbuild(transitive)Clears
vitestGHSA-5xrq-8626-4rwp (critical) andviteGHSA-c27g-q93r-2cwf and GHSA-fx2h-pf6j-xcff (high).vite 7 rather than 6, because the
esbuildfix is only reachable there:vite@6.4.3requestsesbuild@^0.25.0and cannot select the patched 0.28.1, whilevite@7.3.6requests^0.27.0 || ^0.28.0and reaches it natively — which is why noesbuildpin is needed.vite-plugin-checker@0.11.0supportsvite >=5.4.20while still acceptingeslint >=7; only 0.12.0 raises that toeslint >=9.39.1. Stopping at 0.11.0 reaches vite 7 without the ESLint 8 → 9 flat-config port, soeslintandtypescriptare unchanged here.Two peer warnings are expected and benign:
vite-plugin-node-stdlib-browser@0.2.1declaresvite ^2 || ^3 || ^4. 0.2.1 is its latest release and it has not been updated for vite 5+. It works: the production build succeeds andvm-browserifyis still bundled, which is the polyfill doing its job. Worth replacing with a maintained equivalent if it ever breaks, but it is not broken.@types/node@17.0.8does not satisfy the range vite requests.tscreports zero errors, so it was left alone rather than bumped inside a security change.Medium-severity pass
A second pass cleared the remainder, almost all of it the same staleness pattern — patched versions already admitted by the ranges their parents request:
micromatch4.0.8 ·browserslist4.28.7 ·word-wrap1.2.5 ·path-parse1.0.7 ·bn.js4.12.5 ·@sideway/formula3.0.1 ·joi17.13.4 ·yaml1.10.3 ·ajv6.15.0 ·@tootallnate/once2.0.1 ·@babel/helpersand@babel/runtime-corejs37.29.7.Plus two direct-dependency changes:
react-router-dom→ 6.30.4, the patched release on the 6.x line (clears GHSA-2j2x-hqr9-3h42).@babel/coreand@babel/preset-typescriptfrom exact7.16.7to ranges. The tree carried two Babel versions, becausebabel-preset-react-appandeslint-config-react-apprequest^7.16.0and the exact pin could not reach it. They now collapse onto 7.29.7.Removed
babel-core@6.26.3— a direct devDependency and the only path tobabel-traverse@6.26.0, a critical with no published fix on any version. Nothing required it:babel-eslintdepends on@babel/traverse@^7.7.0, not Babel 6, and it appears in no source file or config.postcss-rtl@1.7.3— a direct devDependency and the only path tortlcss@2.5.0→postcss@6.0.23(2 high). There is no PostCSS config file in the repository and no reference to it in the build.path-to-regexp: 1.9.0resolution — inherited from the skeleton. Nopath-to-regexpentry existed in thisyarn.lockat all, so the constraint matched nothing and removing it left the lockfile byte-identical. That is conclusive here, but it is the strong case: as the four retirements above show, a constraint can change the lockfile and still change nothing installed, so the general test is whether a resolved version moves. (The skeleton has since retired this pin too, having found it inert in its own tree as well.)form-data,jszip,minimatchandbrace-expansionresolutions — see above.The inherited exact versions, reopened into ranges
@babel/runtime,i18next,glob-parent,moment,nanoid,node-fetch,diff,debugandua-parser-jswere added as exact versions by the copied February 2026 commit with no explanation. By their own descriptions elsewhere, several are security floors — and a floor written as a fixture cannot receive a patch, which is exactly howaxios: 1.13.5andqs: 6.14.2above became the findings they were added to prevent.None of them was inside a live advisory range, checked against the published advisory database, so this is not a remediation — it closes a decay path before it opens. Eight were converted; a caret keeps every consumer on the major the exact version already forced it onto, so nothing changes line.
@babel/runtime: ^7.26.10node-fetch: ^2.6.7moment: ^2.29.4diff: ^5.2.2nanoid: ^3.3.8debug: ^4.3.4glob-parent: ^5.1.2ua-parser-js: ^0.7.24i18nextis deliberately left exact. It sits far below the current release line and a resolution on it can break application startup, so it changes only with an app-startup check behind it.Two things surfaced while doing this.
nanoid: 3.3.8was holdingpostcssbelow the^3.3.16it declares, andnode-fetch: 2.6.7was holding a consumer below its declared^2.7.0— a constraint holding a package beneath its own parent's range is worth treating as a signal in itself, because it looks like protection and is the opposite. And the directmomentdependency still said2.29.4while its resolution allowed more, the same contradictionaxiosandlodashhad.Pruning these remains open and has to be judged one at a time.
glob-parentis the likeliest candidate: it is a global entry and the tree has consumers declaring^3.1.0and^6.0.1that it pulls onto the 5.x line.Install policy
.yarnrc.ymldeclared onlynodeLinker, so three of the four supply-chain settings fell back to yarn's defaults — looser than what the other repositories in this family run. They were never decided; they were simply never declared.npmMinimalAgeGate07denableScriptstruefalseenableHardenedModenulltruechecksumBehaviorthrowthrow(already matched)The lockfile comes back byte-identical, so this costs nothing today. Two notes for whoever works here next, both now in
RESOLUTIONS.md:yarn up -Rreports success while selecting one patch short rather than failing. Compare the version you got against the version the advisory names.enableScripts: falseproducesYN0004foresbuildand threecore-jsvariants. Neither breaks —esbuildships its binary as a platform-specific optional package rather than fetching it in a postinstall, verified by running a transform through it. A package that genuinely needed its postinstall would fail, so a newYN0004is worth checking rather than ignoring.Tooling and references
Findings were triaged with
/sca-triage, which fetches the SARIF for the branch and classifies every finding into buckets. It turns a raw alert list into a worklist quickly, and that is worth having — but its classification is a starting point, not a verdict:@babel/traversecriticals and the@babel/plugin-transform-modules-systemjshigh asunfixed-upstream. They are not: GHSA-67hx-6x53-jw92 is patched in@babel/traverse@7.23.2, and the "no patched version" row of that advisory belongs tobabel-traverse, the Babel 6 package.form-data's fix as 2.5.6, a different major line from the installed 4.x.Every bucket was re-checked against the dependency paths, the advisory's affected range and the published version list before acting.
The reference implementation is dhis2-app-skeleton#120, and the approach and the shape of
RESOLUTIONS.mdfollow metadata-synchronization#1061, which established the why / fixes / drop-when contract. Three of #120's decisions did not survive re-verification here — the ESLint 9 migration, which the vite 7 fix does not require; thepath-to-regexppin, which binds nothing in this tree; and theminimatch/brace-expansionapproach, which this tree does not need at all. All three have been fed back to that PR, and the skeleton has since retired thepath-to-regexppin in its own tree.Documentation
RESOLUTIONS.md(new) documents every resolution with why / fixes / drop-when, split into Security floors and Compatibility fixtures with no recorded rationale. It also holds the decisions that leave no trace in the manifest at all, the ones a future maintainer cannot recover by reading the tree: why the archived i18n packages are still here and what should trigger revisiting that, each removed resolution and the evidence for removing it, and the findings with no remediation available.Remaining findings
All medium, and none with a remediation available inside this PR's scope.
react-router-dom@6.30.4>= 6.30.2, <= 6.30.4, and 6.30.4 is the final release of the 6.x line — it is still inside the range. No fix exists below v7react-router@6.30.4>= 6.4.0, < 7.18.0, patched in 7.18.0react-router@6.30.4>= 6.0.0, < 7.18.0, patched in 7.18.0elliptic@6.6.1<= 6.6.1) and 6.6.1 is the latest release. Nothing to upgrade toeslint@8.7.0The three
react-routerfindings share one fix path: migrating to react-router v7, which is an API change on a direct dependency used throughout the routing layer. Bumping to 6.30.4 within this PR did clear a fourth finding (GHSA-2j2x-hqr9-3h42, patched in 6.30.4), but the 6.x line ends there. The migration belongs in its own PR with its own testing, not bundled into a dependency pass.ellipticand the withdrawneslintadvisory are recorded inRESOLUTIONS.mdunder Findings with no remediation available, so neither is investigated again from scratch.🔥 Notes for the reviewer
Run
nvm usefirst.yarn install --immutableyarn lintyarn localizeyarn testtsc --noEmityarn buildmanifest.webappandbulk-load.zipproduced under vite 7yarn npm audit -R --severity highesbuildtransform, run directlymomentformatting with a Spanish localeWhat to check by hand:
momentmoved 2.29.4 → 2.30.1 and this repository formats dates in generated templates, so this is the check that exercises it.yarn install && yarn start— the app loads and a screen is usable. A dependency fix that clears the scanner and breaks the app is the failure mode this change is guarding against.yarn localize— translations regenerate. This still runs throughd2-i18n-generate, unchanged, buthandlebarsmoved underneath it.yarn lint,yarn test,yarn buildincluding the zip step — the dev toolchain moved from vite 4 to vite 7, andminimatchis now unconstrained, so these are what exercise both.yarn why axiosandyarn why lodash— expect 1.19.0 and 4.18.1, selected by the^ranges. If either resolves lower, a parent is requesting an exact version and the floor is not doing its job.yarn why minimatchandyarn why brace-expansion— expect 10.2.6 alongside 3.1.5, and 5.0.9 alongside 1.1.18, with no resolution forcing any of them. Multiple major lines coexisting is the expected state here, not a regression.<Linkify>—linkify-itwas forced across a major. It was checked (callable CJS export,.tlds(),.match(),.test()unchanged, and<Linkify>producing the expected<a href>for URLs andmailto:addresses), but it is worth one look in the running app.📹 Screenshots/Screen capture
No user-facing change — dependencies, build tooling and configuration only.
📑 Others
Follow-up, none of it in scope here:
developmentis not being scanned. It has zero Dependency-Track analyses, because itsmain.ymldoes not forwardDTRACK_API_KEY— the scan jobs are skipped and the workflow still reports success. Add dependency track checks #405 is the change that fixes this. Until it merges there is no baseline for the CI gate, so the numbers here come from a scan of the head branch, not a base-to-head comparison.react-routerv7, which is the only route to the three remaining medium findings. Its own PR — it is an API change on a direct dependency, not a dependency bump.glob-parentis the likeliest to go — it is global, and the tree has consumers on^3.1.0and^6.0.1that it pulls onto the 5.x line.eslintadvisory (GHSA-p5wg-g6qr-c7cg) in code scanning, so it stops being re-triaged. Withdrawn advisories are not remediated, and upgradingeslinton account of it would be work for nothing.