fix(deps): bump brace-expansion out of all three vulnerable ranges (GHSA-3jxr-9vmj-r5cp) - #303
Conversation
…HSA-3jxr-9vmj-r5cp) CVE-2026-13149 / GHSA-3jxr-9vmj-r5cp: brace-expansion is vulnerable to a denial of service via exponential-time expansion of consecutive non-expanding {} groups. The advisory has THREE DISJOINT vulnerable ranges, each with its own patched version: < 1.1.16 -> 1.1.16 >= 2.0.0, < 2.1.2 -> 2.1.2 >= 3.0.0, < 5.0.7 -> 5.0.7 app/package-lock.json resolved brace-expansion at seven places spanning all three ranges, so the single reported floor (5.0.7) would not have covered the 1.x and 2.x lines: 1.1.15 -> 1.1.16 (@electron/asar, dir-compare, glob) 2.1.1 -> 2.1.2 (@electron/universal, filelist, test-exclude) 5.0.6 -> 5.0.8 (hoisted, via minimatch) Each entry moves to the newest release WITHIN ITS OWN MAJOR, so no major boundary is crossed and every parent constraint (minimatch ^1.1.7 / ^2.0.2 / ^5.0.5) is still satisfied. Lockfile-only change; no declared range is pinned to an exact version. brace-expansion 5.0.8 narrows engines.node from "18 || 20 || >=22" to "20 || >=22"; CI runs Node 20, so this is satisfied.
Why
|
| state | commit | result | exit |
|---|---|---|---|
origin/main (this PR's base) |
97600e8 |
9 packages / 18 vulnerabilities | 1 |
| this branch | b8e84f3 |
8 packages / 14 vulnerabilities | 1 |
Failure-set diff: 6 rows cleared, 2 rows appear — and the 2 "new" rows are the same advisory at a higher version, not a new advisory:
- cleared (6):
GHSA-3jxr-9vmj-r5cpat 1.1.15 / 2.1.1 / 5.0.6 andGHSA-mh99-v99m-4gvgat 1.1.15 / 2.1.1 / 5.0.6 - remaining (2):
GHSA-mh99-v99m-4gvgat 1.1.16 / 2.1.2
So this PR fully clears its target advisory GHSA-3jxr-9vmj-r5cp and strictly reduces the total from 18 → 14. main measured today fails identically (in fact worse). Its last green run was 2026-07-18 at this very commit; the database, not the code, is what changed.
The blocker: a second advisory published yesterday
GHSA-mh99-v99m-4gvg — "brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash" — was published 2026-07-24 21:53 UTC. Range <= 5.0.7, patched only in 5.0.8. There is no fix in the 1.x or 2.x lines.
The hoisted 5.0.6 → 5.0.8 bump in this PR clears it. The 1.x and 2.x entries cannot be cleared by any in-major upgrade.
Forcing everything to 5.0.8 is NOT a viable workaround
I checked the export shape mechanically rather than assuming:
1.1.16 export typeof : function 1.1.16 call a{b,c} : [ 'ab', 'ac' ]
5.0.8 export typeof : object 5.0.8 call THREW : "is not a function"
5.0.8 keys : [ 'EXPANSION_MAX', 'EXPANSION_MAX_LENGTH', 'expand' ]
brace-expansion 5.x moved from module.exports = expand to a named { expand } export. minimatch 3.x does var expand = require('brace-expansion'); expand(pattern), so an override forcing 5.x onto the ^1.1.7 / ^2.0.2 consumers would throw at runtime. That is a breaking transitive major migration, not a dependency bump.
Recommendation
This PR is correct and strictly improves the gate, but cannot make it green on its own. Reaching the repo's clean-zero policy additionally needs, all pre-existing and independent of this change:
| package | current | needs |
|---|---|---|
tar (dev) |
7.5.16 | 7.5.21 (5 advisories, one 9.2 Critical) |
fast-uri (dev) |
3.1.2 | 3.1.4 (2 advisories; also in render-cli) |
js-yaml |
4.2.0 | 4.3.0 |
postcss (dev) |
8.5.15 | 8.5.18 (also in render-cli) |
brace-expansion 1.x/2.x |
1.1.16 / 2.1.2 | no in-major fix — needs a minimatch migration or a reasoned osv-scanner.toml ignore per the policy in that file |
Left open deliberately rather than merged: the required quality check is genuinely red, and I did not use --admin.
…ded overrides
Adds bounded security floors for four transitive packages flagged by the
gate-deps osv-scanner step. All four are in-major patch lifts - no major
boundary is crossed, and every existing parent constraint still resolves.
tar 7.5.16 -> 7.5.22 app/package-lock.json (dev; via app-builder-lib ^7.5.7, node-gyp ^7.5.4)
tar 7.5.16 carries FIVE advisories, and the CRITICAL one does NOT have
the highest floor - so fixing to the critical's own patched version would
have left two advisories live:
GHSA-23hp-3jrh-7fpw CRITICAL <= 7.5.18 -> 7.5.19
GHSA-8x88-c5mf-7j5w high <= 7.5.17 -> 7.5.18
GHSA-w8wr-v893-vjvp medium <= 7.5.17 -> 7.5.18
GHSA-gvwx-54wh-qm9j medium <= 7.5.16 -> 7.5.17
GHSA-r292-9mhp-454m medium <= 7.5.20 -> 7.5.21 <-- highest floor
The effective floor is 7.5.21, taken from the MEDIUM published 2026-07-24,
not from the CRITICAL. Override "tar": "^7.5.21" == >=7.5.21 <8. No 8.x
exists, so the upper bound is a guard.
fast-uri 3.1.2 -> 3.1.4 app/ AND app/render-cli/ (dev; via ajv ^3.0.1)
GHSA-4c8g-83qw-93j6 high >= 3.0.0, < 3.1.3 -> 3.1.3
GHSA-v2hh-gcrm-f6hx high >= 3.0.0, <= 3.1.3 -> 3.1.4
Both advisories also carry disjoint 2.x and 4.x ranges. Override
"^3.1.4" == >=3.1.4 <4 keeps us on the 3.x line; 4.x EXISTS (4.1.1), so
here the upper bound is load-bearing, not decorative.
js-yaml 4.2.0 -> 4.3.0 app/package-lock.json (RUNTIME; via electron-updater ^4.1.0)
GHSA-52cp-r559-cp3m high >= 4.0.0, < 4.3.0 -> 4.3.0
Advisory also has a disjoint 3.x range (>= 3.0.0, < 3.15.0 -> 3.15.0).
Override "^4.3.0" == >=4.3.0 <5; 5.x EXISTS (5.2.2), so the upper bound
is load-bearing. This is the only one of the four on the runtime path.
postcss 8.5.15 -> 8.5.23 app/ AND app/render-cli/ (dev; via vite ^8.5.3, css-loader ^8.4.40)
GHSA-r28c-9q8g-f849 high <= 8.5.17 -> 8.5.18
Override "^8.5.18" == >=8.5.18 <9. No 9.x exists, so a guard.
Bounded floors, never exact pins, so Dependabot can keep proposing upgrades
within each major. Uses the caret form already established in this repo for
security floors (esbuild ^0.28.1, undici ^6.27.0, render-cli ws ^8.21.0).
Measured with the CI-pinned osv-scanner 2.3.8 over blob-exact `git archive`
extracts of both states: 18 vulnerabilities / 9 packages -> see PR for the
post-change number and the exact remaining residual.
Update — residual swept: 18 → 2 vulnerabilities, CRITICAL clearedPushed Measured with the CI-pinned
|
| state | commit | packages | vulnerabilities | breakdown |
|---|---|---|---|---|
origin/main |
97600e8 |
9 | 18 | 1 Critical, 14 High, 3 Medium |
| branch, first push | b8e84f3 |
8 | 14 | 1 Critical, 10 High, 3 Medium |
| branch now | b4ef66c |
2 | 2 | 0 Critical, 2 High, 0 Medium |
The tar CRITICAL — and why the critical's own floor was not enough
tar 7.5.16 carried five advisories, and the highest floor comes from a medium, not the critical:
| advisory | severity | range | patched |
|---|---|---|---|
GHSA-23hp-3jrh-7fpw |
CRITICAL | <= 7.5.18 |
7.5.19 |
GHSA-8x88-c5mf-7j5w |
high | <= 7.5.17 |
7.5.18 |
GHSA-w8wr-v893-vjvp |
medium | <= 7.5.17 |
7.5.18 |
GHSA-gvwx-54wh-qm9j |
medium | <= 7.5.16 |
7.5.17 |
GHSA-r292-9mhp-454m |
medium | <= 7.5.20 |
7.5.21 |
Fixing to the critical's own 7.5.19 would have left two advisories live. Effective floor 7.5.21; resolved 7.5.22.
All four floors
| package | before → after | override | upper bound |
|---|---|---|---|
tar (dev) |
7.5.16 → 7.5.22 | ^7.5.21 = >=7.5.21 <8 |
guard — no 8.x exists |
fast-uri (dev, both lockfiles) |
3.1.2 → 3.1.4 | ^3.1.4 = >=3.1.4 <4 |
load-bearing — 4.1.1 exists |
js-yaml (runtime) |
4.2.0 → 4.3.0 | ^4.3.0 = >=4.3.0 <5 |
load-bearing — 5.2.2 exists |
postcss (dev, both lockfiles) |
8.5.15 → 8.5.23 | ^8.5.18 = >=8.5.18 <9 |
guard — no 9.x exists |
Bounded floors, never exact pins, using the caret form already established here for security floors (esbuild ^0.28.1, undici ^6.27.0, render-cli ws ^8.21.0). Note fast-uri and js-yaml advisories each carry multiple disjoint ranges (fast-uri: 2.x/3.x/4.x; js-yaml: 3.x/4.x) — the floors were chosen per-line, same discipline as brace-expansion.
js-yaml is the only one on the runtime path (via electron-updater), so the build/test gates are the meaningful check on it.
Exact remaining residual — and why it has no non-breaking fix
GHSA-mh99-v99m-4gvg | 7.5 | npm | brace-expansion (dev) | 1.1.16 | 5.0.8 | app/package-lock.json
GHSA-mh99-v99m-4gvg | 7.5 | npm | brace-expansion (dev) | 2.1.2 | 5.0.8 | app/package-lock.json
Published 2026-07-24, range <= 5.0.7, fixed only in 5.0.8 — there is no 1.x or 2.x remedy. I measured both candidate upgrade paths and both break at runtime:
Path 1 — force brace-expansion to 5.x:
1.1.16 typeof: function → one('a{b,c}') = ['ab','ac']
5.0.8 typeof: object → five('a{b,c}') THREW "is not a function"
keys: [EXPANSION_MAX, EXPANSION_MAX_LENGTH, expand]
minimatch 3.x does expand = require('brace-expansion'); expand(p).
Path 2 — force the parent minimatch to 10.x:
minimatch 3.1.2 typeof: function → three('a/b.js','a/*.js') = true
minimatch 10.x typeof: object → ten(...) THREW "is not a function"
keys: [minimatch, sep, GLOBSTAR, filter, defaults, braceExpand, makeRe, match]
@electron/asar (^3.0.4), dir-compare (^3.0.5) and glob (^3.1.1) all require minimatch 3.x and call it as a function.
The three 1.1.16 entries come from @electron/asar, dir-compare and glob → minimatch 3.1.5; the three 2.1.2 entries from @electron/universal/test-exclude → minimatch 9.0.9 and filelist → minimatch 5.1.9. The real fix is upstream in the electron-builder chain moving off minimatch 3.x.
Status
gate-deps will still exit 1 on these 2 dev-scope findings, so this PR remains blocked and I have not merged it. I did not use --admin, and I deliberately did not add an osv-scanner.toml ignore — that file's policy allows one for a "genuinely-unfixable vuln … with a reason + dated review note", which this now demonstrably is, but that is a security-posture decision for the maintainer, not something to slip into a dependency PR.
If you want the ignore, the evidence above is the review note; scoped to GHSA-mh99-v99m-4gvg it would take gate-deps to exit 0. Caveat worth recording: an osv-scanner ignore is keyed by vuln ID, not by version, so it would also mask a future regression of the hoisted 5.0.8 entry below 5.0.8.
…, unfixable) Adds a single narrow, dated [[IgnoredVulns]] entry so gate-deps can reach exit 0 and the tar CVSS 9.2 CRITICAL fix in this PR can land. The trade this resolves: a CRITICAL (GHSA-23hp-3jrh-7fpw, node-tar decompression/parse DoS) that is fixed and CI-verified in this PR was being held out of main by a DEV-SCOPE moderate with no fix path. Keeping the critical live in production code to avoid suppressing a dev-only finding is strictly worse security. GHSA-mh99-v99m-4gvg qualifies as "genuinely unfixable" under the POLICY at the top of osv-scanner.toml. Range <= 5.0.7, fixed ONLY in 5.0.8; there is no 1.x or 2.x release with the fix, so no in-major upgrade exists for the two remaining resolutions (1.1.16 and 2.1.2). BOTH escape routes were executed and both break at runtime: 1. force brace-expansion to ^5.0.8 -> 5.x moved its CJS export from a callable to a named object; minimatch 3.x does `expand = require('brace-expansion'); expand(p)` -> throws. 2. lift the parent minimatch to ^10 -> 10.x also exports a named object; @electron/asar (^3.0.4), dir-compare (^3.0.5) and glob (^3.1.1) all call minimatch as a function -> throws identically. Scope is development only. Corroborated independently: GitHub's own Dependabot auto-triage auto-dismisses this advisory on scope `development` in a sibling fleet repo, and momentstudio already carries a dated ignore for the same advisory with its own measured evidence. The reason field carries the caveat that matters: an osv-scanner ignore is keyed by VULNERABILITY ID, not by version, so this entry ALSO suppresses the advisory for the hoisted 5.0.8 resolution where a real fix does exist. Both mitigations (the 5.0.8 lockfile pin, and Dependabot alerting independently of this file) and the two concrete re-check conditions are recorded inline. ignoreUntil = 2026-10-25; removing the ignore is the default outcome. Scope of this commit: ONE advisory id. Nothing else was folded in to reach green. Detector validated in all three states before trusting the green (osv-scanner 2.3.8, the CI-pinned version, with the exact CI flags): unknown key injected -> exit 127, no "Filtered" line, ABORTS without scanning (fail-closed, not fail-open) ignore id non-matching -> exit 1, both findings return this entry as committed -> exit 0, "Filtered 2 vulnerabilities from output", "No issues found" Note for future readers: an unknown key yields exit 127 here, not 130.
|
| state | commit | packages | vulnerabilities |
|---|---|---|---|
origin/main |
97600e8 |
9 | 18 (1 Critical, 14 High, 3 Medium) |
| + upgrades | b4ef66c |
2 | 2 (0 Critical) |
| + one ignore | 42fe7b2 |
0 | 0 — No issues found |
Detector validated in all three states before trusting the green
A config that silently disables scanning is the failure mode here, so I controlled for it rather than trusting the green:
| state | exit | Filtered line |
behaviour |
|---|---|---|---|
| unknown key injected | 127 | absent | aborts without scanning — Failed to read config file: unknown keys in config file: IgnoredVulns.bogusUnknownKey. Fail-closed, not fail-open. |
ignore id pointed at a non-matching GHSA |
1 | absent | both findings return — proves the suppression is narrowly scoped to this one id |
| this entry as committed | 0 | Filtered 2 vulnerabilities from output |
No issues found |
Two notes for future readers: an unknown key yields exit 127, not 130; and osv-scanner 2.3.8 aborts on a bad config rather than falling back to an empty one, so a broken config cannot masquerade as a clean scan.
Schema kept to the three valid keys only — id, ignoreUntil (bare unquoted date 2026-10-25), reason.
Why this PR still cannot merge — NOT this change
gh pr merge (no --admin) returns: "the base branch policy prohibits the merge." The cause is an active repository ruleset, separate from branch protection:
- ruleset
18276553— "CodeQL security red-blocker (any severity)", enforcement active, targeting~DEFAULT_BRANCH, rulecode_scanningwithsecurity_alerts_threshold: all/alerts_threshold: all, andbypass_actors: []. - CodeQL therefore must supply results for the PR — but it does not run.
code-scanning/default-setupreportsstate: not-configured, and the most recent CodeQL analysis on this repo is 2026-07-18T00:21:14. No CodeQL run exists on this branch; onlyqualityran. - Open code-scanning alerts: 0. So this is not an alert blocking the merge — it is the absence of the required analysis.
Corroboration that this is repo-wide and pre-existing, not caused by this PR:
- the last merge into
mainwas chore(deps): bump the npm-app-render-cli-patch-minor group in /app/render-cli with 4 updates #296 at 2026-07-18T00:19:25 — two minutes before the final CodeQL analysis; - all five open PRs are
BLOCKED, including four untouched Dependabot PRs (chore(deps): bump actions/checkout from 6.0.3 to 6.1.0 in the github-actions-root-patch-minor group #299, chore(deps-dev): bump electron from 43.1.0 to 43.2.0 in /app in the npm-app-patch-minor group #300, chore(deps): bump the npm-app-render-cli-patch-minor group across 1 directory with 6 updates #301, chore(deps): bump the pip-sidecar-patch-minor group in /sidecar with 9 updates #302).
Answering the question directly: is the tar CRITICAL closed on main?
No — not yet. Read from main authoritatively:
| package | main |
this branch |
|---|---|---|
tar |
7.5.16 (CVSS 9.2 GHSA-23hp-3jrh-7fpw live) |
7.5.22 |
fast-uri |
3.1.2 | 3.1.4 |
js-yaml |
4.2.0 | 4.3.0 |
postcss |
8.5.15 | 8.5.23 |
main also still carries 0 [[IgnoredVulns]] blocks. The fix is verified and ready on this branch; it lands the moment the merge policy can be satisfied.
What unblocks it (maintainer action — I did not touch protection, rulesets, or use --admin)
Any one of:
- Re-enable CodeQL (default setup or an advanced-setup workflow) so the ruleset's required analysis is produced — this also unblocks the four stuck Dependabot PRs;
- adjust/disable ruleset
18276553; gh pr merge 303 --adminas a one-off, accepting the documented green above.
If you prefer it to land by itself once the policy clears, gh pr merge 303 --squash --delete-branch --auto respects the ruleset fully and is not a bypass — I deliberately did not enable it, since resolving the deadlock is a maintainer decision.
Dependabot alert #274 —
brace-expansion(high)Advisory: GHSA-3jxr-9vmj-r5cp / CVE-2026-13149 — DoS via exponential-time expansion of consecutive non-expanding
{}groups.Manifest:
app/package-lock.json(scope: development)Why the reported floor was not enough
The alert reports
first_patched_version: 5.0.7. That is only the floor for one of the advisory's three disjoint vulnerable ranges:< 1.1.16>= 2.0.0, < 2.1.2>= 3.0.0, < 5.0.7This lockfile resolved
brace-expansionat seven places spanning all three ranges. Bumping everything to>= 5.0.7would have crossed a major on parents that constrain^1.1.7and^2.0.2.Fix — newest release within each existing major
@electron/asar/node_modules/brace-expansion< 1.1.16dir-compare/node_modules/brace-expansion< 1.1.16glob/node_modules/brace-expansion< 1.1.16@electron/universal/node_modules/brace-expansion>= 2.0.0, < 2.1.2filelist/node_modules/brace-expansion>= 2.0.0, < 2.1.2test-exclude/node_modules/brace-expansion>= 2.0.0, < 2.1.2brace-expansion(hoisted, viaminimatch)>= 3.0.0, < 5.0.7No major boundary is crossed; every parent constraint (
minimatch^1.1.7/^2.0.2/^5.0.5) still resolves. Lockfile-only — no declared range is pinned to an exact version, so Dependabot can keep proposing future upgrades.Verification
brace-expansion@5.0.8narrowsengines.nodefrom18 || 20 || >=22to20 || >=22. All three workflows pinnode-version: "20", so this is satisfied.