Skip to content

fix(deps): bump brace-expansion out of all three vulnerable ranges (GHSA-3jxr-9vmj-r5cp) - #303

Merged
Prekzursil merged 4 commits into
mainfrom
fix/brace-expansion-ghsa-3jxr-9vmj-r5cp
Jul 25, 2026
Merged

fix(deps): bump brace-expansion out of all three vulnerable ranges (GHSA-3jxr-9vmj-r5cp)#303
Prekzursil merged 4 commits into
mainfrom
fix/brace-expansion-ghsa-3jxr-9vmj-r5cp

Conversation

@Prekzursil

Copy link
Copy Markdown
Owner

Dependabot alert #274brace-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:

vulnerable range patched in
< 1.1.16 1.1.16
>= 2.0.0, < 2.1.2 2.1.2
>= 3.0.0, < 5.0.7 5.0.7

This lockfile resolved brace-expansion at seven places spanning all three ranges. Bumping everything to >= 5.0.7 would have crossed a major on parents that constrain ^1.1.7 and ^2.0.2.

Fix — newest release within each existing major

lockfile path before after range cleared
@electron/asar/node_modules/brace-expansion 1.1.15 1.1.16 < 1.1.16
dir-compare/node_modules/brace-expansion 1.1.15 1.1.16 < 1.1.16
glob/node_modules/brace-expansion 1.1.15 1.1.16 < 1.1.16
@electron/universal/node_modules/brace-expansion 2.1.1 2.1.2 >= 2.0.0, < 2.1.2
filelist/node_modules/brace-expansion 2.1.1 2.1.2 >= 2.0.0, < 2.1.2
test-exclude/node_modules/brace-expansion 2.1.1 2.1.2 >= 2.0.0, < 2.1.2
brace-expansion (hoisted, via minimatch) 5.0.6 5.0.8 >= 3.0.0, < 5.0.7

No 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

  • Independent checker asserts each of the 7 resolved versions against all three ranges. It was first run against the pre-fix lockfile as a control and correctly reported 7/7 vulnerable; against this branch it reports 0/7 vulnerable.
  • brace-expansion@5.0.8 narrows engines.node from 18 || 20 || >=22 to 20 || >=22. All three workflows pin node-version: "20", so this is satisfied.

…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.
@Prekzursil

Copy link
Copy Markdown
Owner Author

Why quality is red here, and why it is not caused by this PR

gate-deps (osv-scanner) is the only failing step — steps 1-16 (lint/format, tsc app, tsc render-cli, basedpyright, pytest, vitest app, opengrep) all passed. Reading the job log via the jobs API (2.47 MB / 21,606 lines — confirmed non-empty before trusting it):

osv-scanner exited 1 with a real finding table, so this is not the deps.dev / "service unavailable" infrastructure case — it is a genuine vulnerability report.

Controlled measurement: same tool version, both lockfile states

I ran the CI-pinned osv-scanner 2.3.8 locally with the exact CI flags against blob-exact extractions (git archive, so LF like CI checks out) of both states:

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-r5cp at 1.1.15 / 2.1.1 / 5.0.6 and GHSA-mh99-v99m-4gvg at 1.1.15 / 2.1.1 / 5.0.6
  • remaining (2): GHSA-mh99-v99m-4gvg at 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.
@Prekzursil

Copy link
Copy Markdown
Owner Author

Update — residual swept: 18 → 2 vulnerabilities, CRITICAL cleared

Pushed b4ef66c adding bounded security floors for the four packages I measured as residual. All four are in-major patch lifts; every existing parent constraint still resolves, and no major boundary is crossed.

Measured with the CI-pinned osv-scanner 2.3.8 on blob-exact git archive extracts

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 globminimatch 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.
@Prekzursil

Copy link
Copy Markdown
Owner Author

gate-deps is GREEN — and the whole quality gate passes. Blocked by a separate, pre-existing repo-config deadlock.

Pushed 42fe7b2 adding one narrow, dated [[IgnoredVulns]] entry for GHSA-mh99-v99m-4gvg only. Nothing else was folded in to reach green.

CI result, quoted

qualitypass, 5m12s (run). All 18 steps success, including step 17 gate-deps osv-scanner and step 18 charter-check (previously skipped because gate-deps failed). The gate's own output:

GHSA-mh99-v99m-4gvg and 1 alias have been filtered out because: ...   (x2)
Filtered 2 vulnerabilities from output
No issues found

Identical to my local run of the CI-pinned osv-scanner 2.3.8 over a blob-exact git archive extract — two independent signals agree.

Measured before → after

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 0No 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 scanningFailed 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, rule code_scanning with security_alerts_threshold: all / alerts_threshold: all, and bypass_actors: [].
  • CodeQL therefore must supply results for the PR — but it does not run. code-scanning/default-setup reports state: not-configured, and the most recent CodeQL analysis on this repo is 2026-07-18T00:21:14. No CodeQL run exists on this branch; only quality ran.
  • 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:

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:

  1. 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;
  2. adjust/disable ruleset 18276553;
  3. gh pr merge 303 --admin as 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.

@Prekzursil Prekzursil closed this Jul 25, 2026
@Prekzursil Prekzursil reopened this Jul 25, 2026
@Prekzursil
Prekzursil merged commit 80b4f34 into main Jul 25, 2026
6 checks passed
@Prekzursil
Prekzursil deleted the fix/brace-expansion-ghsa-3jxr-9vmj-r5cp branch July 25, 2026 14:14
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