fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser - #2356

Open
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps
Open

fix(deps): clear audit advisories by dropping rimraf/minimatch and bumping fast-xml-parser#2356
rishigupta1599 wants to merge 2 commits into
masterfrom
fix/per-10247-audit-deps

Conversation

@rishigupta1599

@rishigupta1599rishigupta1599 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

yarn audit --groups dependencies on the published production closure reported 5 advisories (4 high, 1 moderate). All reached us transitively:

AdvisoryPath
brace-expansion (ReDoS)minimatchglobrimraf@3 (@percy/core)
brace-expansion (ReDoS)minimatch@9 (@percy/cli-doctor)
inflight (memory leak)globrimraf@3
fast-xml-parserdirect dependency of @percy/core

Why not the override the ticket recommends

PER-10247 recommends pinning brace-expansion@1 → 1.1.12 / @2 → 2.0.2 via resolutions. That does not clear the audit — only 5.0.8 satisfies the advisory, and 5.0.8 requires Node 20+. Requiring a newer Node is exactly what broke the reporting customer, so re-pinning trades one break for another. This PR removes the dependency chains instead.

Changes

  • @percy/core: drop rimraf@3.Browser#close cleans the temp profile with fs.promises.rm({ recursive: true, force: true, maxRetries: 3, retryDelay: 100 }). The retry options preserve rimraf's busy-retry behaviour — on Windows the profile dir stays locked briefly after the browser exits, and a plain rm would throw EBUSY.
  • scripts/test.js: same swap for the coverage-dir cleanup (the only other rimraf consumer).
  • @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch semantics (* and ? — no **, braces, or character classes), so it is a small self-contained matcher now. 6 new specs cover the semantics, including the cases where shExpMatch and minimatch differ.
  • @percy/core: fast-xml-parser^4.4.1^4.5.7.

On fast-xml-parser: why 4.5.7 and not 5.x

My first push bumped this to ^5.7.0 and broke yarn install on every CI job, all of which pin Node 14:

error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"

^5.7.0 floats to 5.10.1, which depends on xml-naming (engines >=16). Pinning fxp alone does not help — 5.7.x depends on fast-xml-builder ^1.1.7, and fast-xml-builder@1.2.0+ pulls xml-naming in as well. Holding Node 14 on 5.x would require a resolutions pin on a dependency-of-a-dependency — the same class of fragile pin this PR set out to avoid.

So this takes the top of the 4.x line (4.5.7, npm legacy tag), which clears every fast-xml-parser advisory that has a 4.x fix: GHSA-m7jm (CRITICAL, patched 4.5.4), GHSA-8gc5 (HIGH, 4.5.5), GHSA-jp2q (MODERATE, 4.5.5), GHSA-fj3w (LOW, 4.5.4).

One advisory remains and is deliberately accepted:GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable range < 5.7.0) — XMLBuilder comment/CDATA injection via unescaped delimiters. It is not reachable from our code: maestro-hierarchy.js is the only consumer, it imports XMLParser only (never XMLBuilder), and parses with processEntities: false.

So the honest score is 5 advisories → 1, not zero.

Decision: hold at 4.5.7 until the Node floor moves to ≥16. Reaching literal zero is not viable today:

  • Every published package declares "engines": { "node": ">=14" }. fxp ≥5.7.0 transitively requires Node ≥16, so shipping it would violate our own published contract.
  • A root resolutions pin on fast-xml-builder (to keep xml-naming out) does not help consumersresolutions/overrides are not published. A customer running npm install @percy/cli resolves from our declared ranges, pulls fast-xml-builder@1.3.0xml-naming (>=16), and hits the same engine failure CI hit. That would clean our audit while degrading their install — the exact failure mode PER-10247 was filed about.

The remaining advisory is unreachable in our code, so this is a documented accept, to be revisited when the Node floor is raised.

Verification

  • Production closure computed from yarn.lock (127 external packages, prod deps only, seeded from each workspace package's dependencies): rimraf, minimatch, glob, inflight, brace-expansion all absent; no Node-16-only package present; fast-xml-parser resolves to 4.5.7 with strnum@1.1.2 as its only dependency (strnum has no advisories).
  • @percy/cli-doctor: 514/514 specs pass (508 existing + 6 new). Independent confirmation that minimatch@9 is gone: master's pac.js can no longer even load in this tree.
  • Repo-wide lint clean.
  • fs.promises.rm behaviour checked directly: recursive delete, missing-path tolerance under force, and that maxRetries/retryDelay are accepted on our Node floor.
  • @percy/core suite: gating on CI. Local runs here are contaminated by leftover real temp dirs (percy-self-hosted-real-root, percy-bs-tmp-real-root) from an earlier crashed run: core mocks fs with memfs, so the fs.rmSync(recursive, force) cleanup in api.test.js cannot clear real leftovers, and the whole maestro self-hosted group then 404s. Those paths do not exist on a fresh CI machine, where force: true no-ops. Please gate on the CI result rather than my local run.

Ref: PER-10247

🤖 Generated with Claude Code

…mping fast-xml-parser
`yarn audit --groups dependencies` reported 5 advisories (4 high, 1 moderate)
against the published production closure, all reaching us transitively:
brace-expansion (ReDoS) <- minimatch <- glob <- rimraf@3 (@percy/core)
<- minimatch@9 (@percy/cli-doctor)
inflight (memory leak) <- glob <- rimraf@3
fast-xml-parser (ReDoS) <- direct dependency of @percy/core
Rather than re-pinning brace-expansion via a resolutions override, remove the
dependency chains that pull it in:
- @percy/core: drop rimraf@3; Browser#close now cleans the temp profile with
fs.promises.rm({ recursive, force, maxRetries: 3, retryDelay: 100 }). The
retry options preserve rimraf's busy-retry behaviour, which matters on
Windows where the profile dir stays locked briefly after the browser exits.
- scripts/test.js: same swap for the coverage-dir cleanup.
- @percy/cli-doctor: drop minimatch@9. The PAC check only needed shExpMatch
glob semantics (`*` and `?`, no `**`/braces/character classes), so it is
now a small self-contained matcher instead of a full glob engine.
- @percy/core: fast-xml-parser ^4.4.1 -> ^5.7.0.
Note the advisory's own recommendation (brace-expansion 1.1.12 / 2.0.2) does
not clear the audit -- only 5.0.8 does, and that requires Node 20+, which is
what broke the reporting customer. Removing the chains avoids that trap.
Verified: audit on the published prod closure goes 5 -> 0 with the vulnerable
packages absent from the tree; cli-doctor 514/514 specs pass; repo-wide lint
clean; fast-xml-parser v4 vs v5 produce byte-identical output across 7 real
fixtures plus 6 synthetic cases using our exact parser options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rishigupta1599
rishigupta1599 requested a review from a team as a code ownerJuly 28, 2026 19:09
…tive deps
The previous commit bumped fast-xml-parser to ^5.7.0, which broke `yarn
install` on every CI job (all pin Node 14):
error xml-naming@0.3.0: The engine "node" is incompatible with this module.
Expected version ">=16.0.0". Got "14.21.3"
^5.7.0 floats to 5.10.1, which depends on xml-naming (engines: >=16). Pinning
fast-xml-parser alone does not help: 5.7.x depends on fast-xml-builder ^1.1.7,
and fast-xml-builder 1.2.0+ pulls xml-naming in as well, so staying on 5.x
would additionally require a `resolutions` pin on a dependency-of-a-dependency
to hold Node 14 -- the same class of fragile pin this PR set out to avoid.
So take the top of the 4.x line (4.5.7, npm `legacy` tag) instead. That still
clears every fast-xml-parser advisory that has a 4.x fix:
GHSA-m7jm-9gc2-mpf2 (CRITICAL) patched 4.5.4
GHSA-8gc5-j5rx-235r (HIGH) patched 4.5.5
GHSA-jp2q-39xq-3w4g (MODERATE) patched 4.5.5
GHSA-fj3w-jwp8-x2g3 (LOW) patched 4.5.4
One advisory remains unfixed on 4.x: GHSA-gh4j-gqv2-49f6 (MODERATE, vulnerable
range `< 5.7.0`) -- XMLBuilder comment/CDATA injection via unescaped
delimiters. It is not reachable from our code: maestro-hierarchy.js is the only
consumer, it imports XMLParser only (never XMLBuilder), and it parses with
processEntities: false.
Production closure computed from yarn.lock (127 external packages) confirms
rimraf, minimatch, glob, inflight and brace-expansion are all absent, and that
no Node-16-only package is pulled in; fast-xml-parser resolves to 4.5.7 with
strnum 1.1.2 as its only dependency (strnum has no advisories).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rishigupta1599rishigupta1599 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.

expect(shExpMatch('percy.io', '*.percy.io')).toBe(false);
expect(shExpMatch('percy.io', 'percy.i?')).toBe(true);
expect(shExpMatch('percy.ioo', 'percy.i?')).toBe(false);
expect(shExpMatch('percy.io', 'percy.io')).toBe(true);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Medium] No shExpMatch coverage for a match target containing /

No test in the suite calls shExpMatch (directly or via runPacScript) with a target containing / — verified, zero such call sites. Every case uses hostname-like targets.

That gap lands on the one input class whose result actually changed in this PR: minimatch treated / as a path-segment boundary that * would not cross, the new matcher lets * cross it (which is what the PAC spec requires). It is also the only example in the canonical Netscape PAC documentation. So the behaviour change is untested in both directions.

Suggestion: add the spec example, plus one end-to-end case through runPacScript with a full-URL pattern:

it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

Reviewer: stack-code-reviewer

* Greedy single-star backtracking, so worst case is O(str * shexp) with no
* regex construction and no catastrophic backtracking.
*/
export function shExpMatch(str, shexp) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Behaviour change vs minimatch worth a CHANGELOG note

This is not a like-for-like replacement. Verified against the installed minimatch:

  • minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false })false (minimatch will not let * cross /); the new shExpMatchtrue, which is what the PAC spec specifies.
  • minimatch('', '*')false; the new implementation → true.

Both are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call using a path pattern — but they are functional changes riding in under a dependency-hygiene commit, and they can flip a PAC script between DIRECT and PROXY.

Suggestion: no code change; mention in the CHANGELOG and PR description that removing minimatch also fixes full-URL and empty-string matching.

Reviewer: stack-code-reviewer

@rishigupta1599

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2356Head:b7f4131Reviewers: stack-code-reviewer

Summary

Removes rimraf@3 from @percy/core and minimatch@9 from @percy/cli-doctor to clear npm-audit advisories reaching us through brace-expansion, replacing them with fs.promises.rm and a hand-written PAC shExpMatch; bumps fast-xml-parser^4.4.1^4.5.7.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface touched.
HighSecurityInput validation and sanitizationPassshExpMatch keeps the type guards and the 256-char cap; treating brace/bracket/extglob syntax as literal removes an unbounded-expansion vector on attacker-influenced PAC content.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource access.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesPassshExpMatch fuzz-checked against a reference */? matcher over 200k random pairs with zero mismatches; loop terminates (s strictly increases on backtrack); no recursion, so no stack-overflow risk.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassfs.promises.rm(...).catch(...) preserves the prior reject-and-log contract; force: true mirrors rimraf v3's tolerance of a missing path. See Low finding on scripts/test.js.
HighCorrectnessNo race conditions or concurrency issuesPassCleanup still sequenced after process-exit and ws-close.
MediumTestingNew code has corresponding testsPass6 new specs covering wildcards, empty inputs, case sensitivity, literal metacharacters, and a timing-bounded pathological case.
MediumTestingError paths and edge cases testedFailThe one canonical PAC spec example for shExpMatch — a match target containing / — has no coverage, and it is exactly where behaviour changed. See Medium finding.
MediumTestingExisting tests still pass (no regressions)PassCI 46/46 green, including Test @percy/core on both Linux and windows-latest.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data access.
MediumPerformanceLong-running tasks use background jobsN/ANot applicable.
MediumQualityFollows existing codebase patternsPassMatches surrounding style in both packages.
MediumQualityChanges are focused (single concern)PassDependency removal plus the minimum code to replace each dependency.
LowQualityMeaningful names, no dead codePassNo leftover references beyond one explanatory comment.
LowQualityComments explain why, not whatPassThe shExpMatch docstring states the security rationale specifically and correctly.
LowQualityNo unnecessary dependencies addedPassNet removal of two published dependencies; nothing added.

Findings

  • File:packages/core/package.json (the engines field)
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue:@percy/core declares "engines": { "node": ">=14" }, but fs.promises.rm — and its recursive/maxRetries/retryDelay options — landed in Node v14.14.0. On Node 14.0.0–14.13.x, fs.promises.rm is undefined, so the call throws TypeError inside the .then() callback of Browser#close. The throw happens while evaluating the call expression, so the adjacent .catch never applies and this._closed rejects, surfacing at browser close rather than being logged and ignored.
  • Suggestion: Set "engines": { "node": ">=14.14.0" } in packages/core/package.json. One line, and correct independently of this PR.
  • Note on severity: the reviewer rated this High and blocking; I downgraded it to Medium after verifying that packages/core/src/cache/byte-lru.js:244 already calls fs.rmSync — also added in 14.14.0 — and that this line is present on master (1c83908c), not introduced here. So the declared floor was already inaccurate before this PR; what changes is exposure, from an opt-in disk-spill path to the default browser-close path. Combined with 14.0–14.13.x being superseded patch releases of an EOL major (CI runs 14.21.3), that reads as a real defect worth fixing but not a merge blocker. A maintainer who holds the engines contract strictly could fairly call it blocking — flagging the judgement rather than burying it.

  • File:packages/cli-doctor/test/pac.test.js:92
  • Severity: Medium
  • Reviewer: stack-code-reviewer
  • Issue: No test anywhere in the suite calls shExpMatch (directly or through runPacScript) with a match target containing / — verified: zero such call sites. Every case uses hostname-like targets. That matters twice over: it is the only example given in the canonical Netscape PAC documentation (shExpMatch("http://home.netscape.com/people/ari/index.html", "*/ari/*")), and it is precisely the input class whose result changed in this PR (see the Low finding below). The behaviour change is therefore untested in both directions.
  • Suggestion: Add the spec example directly, plus one end-to-end case through runPacScript using a full-URL pattern:
    it('matches wildcards across "/" when matching a full URL (spec: */ari/*)',()=>{expect(shExpMatch('http://home.netscape.com/people/ari/index.html','*/ari/*')).toBe(true);expect(shExpMatch('http://home.netscape.com/people/montulli/index.html','*/ari/*')).toBe(false);});

  • File:packages/cli-doctor/src/checks/pac.js:335
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The replacement is not behaviour-preserving, in two ways that both alter PAC proxy-selection outcomes for customers. Verified against the installed minimatch: minimatch('http://home.netscape.com/people/ari/index.html', '*/ari/*', { dot: true, nocase: false }) returns false, because minimatch treats / as a path-segment boundary that * will not cross; the new shExpMatch returns true, matching the PAC spec. Likewise minimatch('', '*') returns false while the new implementation returns true. Both changes are corrections — the old code silently mis-evaluated any shExpMatch(url, ...) call with a path pattern — but they are functional changes shipping under a dependency-hygiene commit message, and they can flip a PAC script's verdict between DIRECT and PROXY.
  • Suggestion: Call this out in the CHANGELOG and the PR description: the minimatch removal also fixes full-URL and empty-string matching. No code change needed.

  • File:scripts/test.js (coverage-cleanup loop)
  • Severity: Low
  • Reviewer: stack-code-reviewer
  • Issue: The previous new Promise(r => rimraf(pattern, r)) used the resolver as the callback, so any error — not just a missing directory — was silently discarded. The new loop awaits fs.promises.rm with no .catch, so a genuine EPERM/EACCES now aborts the coverage run. This is an improvement in failure visibility, noted because it is a behaviour change rather than a like-for-like swap. The brace pattern {.nyc_output,coverage} was also correctly unrolled into an explicit list, since fs.rm does no brace expansion.
  • Suggestion: None required — intentional and better. Dev-only script, not published, so the engines concern above does not apply here.

Other observations (non-blocking)

  • A reviewer claim I checked and rejected. The reviewer stated that GHSA-mh99-v99m-4gvg is "fixed in 5.0.8, or the 1.x/2.x maintenance backports 1.1.17/2.1.3". Those versions do exist (maintenance-v1, maintenance-v2), but the advisory has a single vulnerable range, <= 5.0.7, with firstPatchedVersion: 5.0.8 — so 1.1.17 and 2.1.3 remain inside it and do not clear this advisory. They fix different brace-expansion advisories (GHSA-3jxr needs ≥1.1.16/≥2.1.2; GHSA-f886 needs ≥1.1.13/≥2.0.3). This supports the PR's approach: removing the chains is what clears GHSA-mh99 on the 1.x/2.x lines, because no backport does.
  • Dev-tree findings remain, correctly out of scope.yarn.lock still resolves rimraf@^3.0.0, rimraf@^3.0.2 and brace-expansion@1.1.11/2.0.1 through dev tooling such as nyc. These are not shipped and not exposed to PAC content. A production-closure walk of the lockfile (127 external packages, seeded from each workspace package's dependencies) confirms rimraf, glob, minimatch, inflight and brace-expansion are all absent from what customers install. If the goal is a fully clean whole-repo yarn audit rather than a clean published closure, that is a separate piece of work.
  • fast-xml-parser^4.5.7 is a sound choice given engines: >=14: the 5.x line reaches xml-naming (engines: >=16) both directly and through fast-xml-builder@1.2.0+, so it cannot be adopted without raising the Node floor. 4.5.7 clears every fxp advisory that has a 4.x fix. GHSA-gh4j-gqv2-49f6 (< 5.7.0) remains and is unreachable here — maestro-hierarchy.js is the only consumer and imports XMLParser, never XMLBuilder.

Verdict: PASS — two Medium findings worth addressing (engines floor, and the untested / case that is exactly where behaviour changed); no Critical or High issues survived verification.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rishigupta1599