fix(deps): migrate off abandoned react-router-dom, fix eslint's own brace-expansion

Two real, forward-compatible fixes for npm audit's high-severity
advisories (not the downgrades npm audit fix --force offers):

- react-router (GHSA-qwww-vcr4-c8h2, RSC CSRF bypass): react-router-dom
  is frozen at 7.18.1, pinning the vulnerable react-router@7.18.1 -- no
  newer react-router-dom release exists pointing at the fixed line.
  react-router itself has shipped the real fix at 8.3.0. Migrated the 9
  files importing from react-router-dom (all using plain
  createBrowserRouter/RouterProvider/useLocation/useNavigate/Outlet, no
  RSC anywhere) to import from react-router directly instead.

- brace-expansion/minimatch (GHSA-mh99-v99m-4gvg): fixed for eslint's
  own dependency chain (minimatch@10.2.5, used by eslint itself,
  storybook, typescript-eslint, swagger-client) via a scoped
  "minimatch@^10" override forcing brace-expansion to the now-published
  5.0.8 patch -- within the range minimatch@10.2.5 already declares
  wanting (^5.0.5), so this isn't a version-pin workaround, just
  unblocking a patch release npm's resolver hadn't picked up.

One advisory remains genuinely unfixable from our side:
eslint-plugin-jsx-a11y pins minimatch@^3.1.2 (old major, never
patched); forcing it to the 10.x line via override breaks npm's own
dependency-tree validation (a real incompatibility, not just an npm
quirk), so this needs an eslint-plugin-jsx-a11y release bumping its own
minimatch. Lint-time only, no untrusted input reaches it -- ci.yml's
Audit step comment updated to reflect the new, smaller remaining scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kuzz007
2026-07-26 23:31:33 +03:00
parent f70002d9a6
commit 032dbabc39
13 changed files with 76 additions and 68 deletions
+20 -11
View File
@@ -180,17 +180,26 @@ jobs:
- name: Build Storybook
run: npm run build-storybook
working-directory: frontend
# Known-accepted as of 2026-07-26: 2 high advisories (react-router via
# react-router-dom, GHSA-qwww-vcr4-c8h2; brace-expansion/minimatch via
# eslint-plugin-jsx-a11y, GHSA-mh99-v99m-4gvg). Neither is exploitable
# here -- the react-router CVE only affects the unstable RSC APIs
# (this app uses plain createBrowserRouter, no RSC anywhere), and the
# jsx-a11y chain only runs against this repo's own hardcoded lint globs,
# never untrusted input. Both "fixes" npm offers are downgrades (no
# patched release exists yet in the currently-depended-upon package
# line), so left as-is rather than trading a real regression for a
# vulnerability that doesn't apply. Re-check on a future bump in case
# upstream ships an actual forward fix.
# Known-accepted as of 2026-07-26: 1 high advisory,
# brace-expansion/minimatch via eslint-plugin-jsx-a11y's own pinned
# minimatch@^3.1.2 (GHSA-mh99-v99m-4gvg). Not exploitable here -- this
# chain only runs against this repo's own hardcoded lint globs, never
# untrusted input. The eslint-core minimatch@10.x instance of the same
# advisory is already fixed via the "minimatch@^10" override below
# (brace-expansion 5.0.8); jsx-a11y's own minimatch is pinned to an old
# major with no patched release in that line, and forcing it to 10.x
# via override breaks npm's own dependency-tree validation (a genuine
# incompatibility, not just an npm quirk) -- so this one can only be
# fixed by an eslint-plugin-jsx-a11y release bumping its own minimatch.
# `npm audit fix --force`'s suggestion is a downgrade to before jsx-a11y
# adopted the vulnerable chain at all; left as-is rather than trading a
# real regression for a vulnerability that doesn't apply.
# The other advisory this comment used to cover (react-router RSC CSRF
# bypass, GHSA-qwww-vcr4-c8h2) is actually fixed now: migrated off the
# abandoned react-router-dom (frozen at 7.18.1, pinning the vulnerable
# react-router 7.18.1) onto react-router 8.3.0 directly, which has the
# real forward fix -- not a downgrade. Re-check on a future bump in
# case upstream ships a real fix for the remaining advisory too.
- name: Audit
run: npm audit --audit-level=high
working-directory: frontend
+19 -35
View File
@@ -25,7 +25,7 @@
"react-dom": "^19.2.8",
"react-hook-form": "^7.82.0",
"react-i18next": "^17.0.10",
"react-router-dom": "^7.18.1",
"react-router": "^8.3.0",
"swagger-ui-react": "^5.32.11",
"uplot": "^1.6.32",
"zod": "^4.4.3"
@@ -5777,15 +5777,15 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/browserslist": {
@@ -6187,6 +6187,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -6196,6 +6197,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/cookie-es": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
"license": "MIT"
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
@@ -10176,20 +10183,19 @@
}
},
"node_modules/react-router": {
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz",
"integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
"cookie-es": "^3.1.1"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.22.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
"react": ">=19.2.7",
"react-dom": ">=19.2.7"
},
"peerDependenciesMeta": {
"react-dom": {
@@ -10197,28 +10203,6 @@
}
}
},
"node_modules/react-router-dom": {
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/react-syntax-highlighter": {
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz",
+7 -4
View File
@@ -44,7 +44,7 @@
"react-dom": "^19.2.8",
"react-hook-form": "^7.82.0",
"react-i18next": "^17.0.10",
"react-router-dom": "^7.18.1",
"react-router": "^8.3.0",
"swagger-ui-react": "^5.32.11",
"uplot": "^1.6.32",
"zod": "^4.4.3"
@@ -61,6 +61,7 @@
"@types/react-dom": "^19.2.3",
"@types/swagger-ui-react": "^5.18.0",
"@vitejs/plugin-react": "^6.0.4",
"@vitest/browser-playwright": "4.1.10",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.7.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
@@ -70,18 +71,20 @@
"jsdom": "^29.1.1",
"lint-staged": "^17.1.1",
"msw": "^2.15.0",
"playwright": "^1.61.1",
"storybook": "^10.5.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
"vite": "8.1.5",
"vitest": "^4.1.10",
"@vitest/browser-playwright": "4.1.10",
"playwright": "^1.61.1"
"vitest": "^4.1.10"
},
"overrides": {
"eslint-plugin-jsx-a11y": {
"eslint": "$eslint"
},
"minimatch@^10": {
"brace-expansion": "^5.0.8"
},
"dompurify": "^3.4.11",
"react-copy-to-clipboard": "^5.1.1",
"react-inspector": "^9.0.0",
+21 -9
View File
@@ -7,8 +7,8 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.7'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const PACKAGE_VERSION = '2.15.0'
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
@@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Omit the body of server-sent event stream responses.
// Cloning such responses would prevent client-side stream cancelations
// from reaching the original stream (a teed stream only cancels its
// source once both of its branches cancel) and would buffer the
// entire stream into the unconsumed clone indefinitely.
const isEventStreamResponse = response.headers
.get('content-type')
?.toLowerCase()
.startsWith('text/event-stream')
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
const responseClone = isEventStreamResponse ? null : response.clone()
sendToClient(
client,
@@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
type: response.type,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseClone ? responseClone.body : null,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
responseClone && responseClone.body
? [serializedRequest.body, responseClone.body]
: [],
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useLocation } from 'react-router';
import { useTranslation } from 'react-i18next';
const TITLE_KEYS: Record<string, string> = {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ComponentType } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Drawer, Layout, Menu } from 'antd';
import type { MenuProps } from 'antd';
+1 -1
View File
@@ -1,4 +1,4 @@
import { Outlet } from 'react-router-dom';
import { Outlet } from 'react-router';
import { useWebSocketBridge } from '@/api/websocketBridge';
import { usePageTitle } from '@/hooks/usePageTitle';
+1 -1
View File
@@ -1,5 +1,5 @@
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { RouterProvider } from 'react-router';
import { message } from 'antd';
import 'antd/dist/reset.css';
import '@/styles/utils.css';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useLocation } from 'react-router';
import {
Alert,
Button,
@@ -1,7 +1,7 @@
import { Alert, Button, Input, InputNumber, Switch, Tabs } from 'antd';
import { BranchesOutlined, CompassOutlined, IdcardOutlined, InfoCircleOutlined, NodeIndexOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router';
import type { AllSetting } from '@/models/setting';
import { SettingListItem } from '@/components/ui';
import { RemarkTemplateField } from '@/components/form';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router';
import {
Alert,
Button,
+1 -1
View File
@@ -1,5 +1,5 @@
import { lazy, Suspense } from 'react';
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import { createBrowserRouter, type RouteObject } from 'react-router';
import PanelLayout from '@/layouts/PanelLayout';
@@ -1,5 +1,5 @@
import { fireEvent, screen } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { MemoryRouter, useLocation } from 'react-router';
import { describe, expect, it, vi } from 'vitest';
import { AllSetting } from '@/models/setting';