From 03a4d7aa44d4999a101f5b85b022b59f7818b8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Mu=C3=B1oz?= Date: Tue, 29 Sep 2026 16:28:18 -0700 Subject: [PATCH] Fix decode-uri-component DoS in frontend Upgrade query-string and use-query-params to consume the fixed decode-uri-component 0.5.0 through a supported ESM dependency chain. Migrate imports and React Router integration while preserving existing query-string serialization behavior. This avoids forcing the ESM-only decoder into CommonJS query-string v7, which would leave an unsupported Node/tooling configuration. --- src/gprofiler/frontend/package.json | 6 +- src/gprofiler/frontend/src/App.jsx | 10 +-- .../api/downloadInstallationTemplateFile.js | 4 +- .../src/api/hooks/useGetCompareFgData.jsx | 9 ++- .../src/api/hooks/useGetCpuAndMemoryTrend.jsx | 4 +- .../api/hooks/useGetDatesTimesWithData.jsx | 4 +- .../frontend/src/api/hooks/useGetFgData.jsx | 6 +- .../src/api/hooks/useGetFgMetrics.jsx | 16 +++-- .../api/hooks/useGetServiceMemoryAndCpu.jsx | 4 +- .../api/hooks/useGetServiceNodesAndCores.jsx | 4 +- .../src/api/hooks/useGetServiceSamples.jsx | 4 +- .../api/hooks/useGetTableFunctionCpuData.jsx | 12 +++- src/gprofiler/frontend/src/api/urls.js | 4 +- .../profiles/header/ProfilesActions.jsx | 4 +- .../views/adhoc/AdhocProfilingView.jsx | 18 ++++-- src/gprofiler/frontend/src/utils/fgUtils.js | 36 ++++++----- src/gprofiler/frontend/yarn.lock | 62 +++++++++---------- 17 files changed, 114 insertions(+), 93 deletions(-) diff --git a/src/gprofiler/frontend/package.json b/src/gprofiler/frontend/package.json index 5aaed13b..abe8922a 100644 --- a/src/gprofiler/frontend/package.json +++ b/src/gprofiler/frontend/package.json @@ -18,7 +18,7 @@ "postcss": "^8.5.18", "browserslist": "^4.28.7", "nanoid": "^3.3.18", - "decode-uri-component": "^0.2.2", + "decode-uri-component": "^0.5.0", "@humanfs/node": "^0.16.8" }, "dependencies": { @@ -41,7 +41,7 @@ "lodash": "^4.18.1", "match-sorter": "^6.3.1", "prop-types": "^15.7.2", - "query-string": "^7.0.1", + "query-string": "^9.5.1", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-csv": "^2.2.2", @@ -49,7 +49,7 @@ "react-error-boundary": "^3.1.3", "react-router-dom": "^5.2.0", "react-window": "^1.8.6", - "use-query-params": "^1.2.3" + "use-query-params": "^2.2.2" }, "scripts": { "start": "vite", diff --git a/src/gprofiler/frontend/src/App.jsx b/src/gprofiler/frontend/src/App.jsx index a7ccce83..8c390742 100644 --- a/src/gprofiler/frontend/src/App.jsx +++ b/src/gprofiler/frontend/src/App.jsx @@ -19,15 +19,15 @@ import { CssBaseline } from '@mui/material'; import { ThemeProvider } from '@mui/material/styles'; import { ErrorBoundary } from 'react-error-boundary'; -import { Route } from 'react-router-dom'; -import { QueryParamProvider, transformSearchStringJsonSafe } from 'use-query-params'; +import { objectToSearchString, QueryParamProvider, transformSearchStringJsonSafe } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import ErrorFallback from './components/common/feedback/ErrorFallback'; import Console from './components/console/Console'; import theme from './theme/theme'; -const queryStringifyOptions = { - transformSearchString: transformSearchStringJsonSafe, +const queryParamOptions = { + objectToSearchString: (query) => transformSearchStringJsonSafe(objectToSearchString(query)), }; function App() { @@ -35,7 +35,7 @@ function App() { - + diff --git a/src/gprofiler/frontend/src/api/downloadInstallationTemplateFile.js b/src/gprofiler/frontend/src/api/downloadInstallationTemplateFile.js index ab837f79..cd0e22de 100644 --- a/src/gprofiler/frontend/src/api/downloadInstallationTemplateFile.js +++ b/src/gprofiler/frontend/src/api/downloadInstallationTemplateFile.js @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { getJSON } from './utils'; @@ -39,7 +39,7 @@ const downloadInstallationTemplateFile = async ( const queryData = { serviceName, namespace }; let result; setIsLoading(true); - const url = `/api/installations/${type}/file?${stringify(queryData)}`; + const url = `/api/installations/${type}/file?${queryString.stringify(queryData)}`; const response = await fetch(url, { headers: { 'Content-Type': 'application/json' }, method: 'GET', diff --git a/src/gprofiler/frontend/src/api/hooks/useGetCompareFgData.jsx b/src/gprofiler/frontend/src/api/hooks/useGetCompareFgData.jsx index 56c8d6ba..f079ce2c 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetCompareFgData.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetCompareFgData.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { DATA_URLS } from '../urls'; import useFetchWithRequest from '../useFetchWithRequest'; @@ -35,13 +35,16 @@ const useGetCompareFgData = ({ timeSelection, service, setAbsoluteCompareTime }) const timeParams = getStartEndDateTimeFromSelection(timeSelection); const { data, loading, error } = useFetchWithRequest( { - url: `${DATA_URLS.GET_FLAMEGRAPH}?${stringify(_.assign({ ...timeParams }, fgParams))}`, + url: `${DATA_URLS.GET_FLAMEGRAPH}?${queryString.stringify(_.assign({ ...timeParams }, fgParams))}`, }, { refreshDeps: [service, timeSelection], ready: areParamsDefined(service, timeSelection), onSuccess: () => { - setAbsoluteCompareTime({ cStartTime: timeParams.startTime, cEndTime: timeParams.endTime }); + setAbsoluteCompareTime({ + cStartTime: timeParams.startTime, + cEndTime: timeParams.endTime, + }); }, } ); diff --git a/src/gprofiler/frontend/src/api/hooks/useGetCpuAndMemoryTrend.jsx b/src/gprofiler/frontend/src/api/hooks/useGetCpuAndMemoryTrend.jsx index 5c744f18..a2a0eae0 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetCpuAndMemoryTrend.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetCpuAndMemoryTrend.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext } from 'react'; import { FilterTagsContext } from '../../states/filters/FiltersTagsContext'; @@ -45,7 +45,7 @@ const useGetCpuAndMemoryTrend = () => { url: DATA_URLS.GET_METRICS_CPU_AND_MEMORY_TREND + '?' + - stringify(_.assign({ ...timeParams }, metricsParams)), + queryString.stringify(_.assign({ ...timeParams }, metricsParams)), }, { refreshDeps: [selectedService, timeSelection, activeFilterTag], diff --git a/src/gprofiler/frontend/src/api/hooks/useGetDatesTimesWithData.jsx b/src/gprofiler/frontend/src/api/hooks/useGetDatesTimesWithData.jsx index ba28d5d7..c360368f 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetDatesTimesWithData.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetDatesTimesWithData.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext, useEffect, useState } from 'react'; import { DATA_URLS } from '../../api/urls'; @@ -36,7 +36,7 @@ const useGetDatesTimesWithData = (primarySelectedTime) => { run, } = useFetchWithRequest( { - url: `${DATA_URLS.GET_FLAMEGRAPH_DATETIME_WITH_DATA}?${stringify({ + url: `${DATA_URLS.GET_FLAMEGRAPH_DATETIME_WITH_DATA}?${queryString.stringify({ serviceName: selectedService, filter: activeFilterTag?.filter ? JSON.stringify(activeFilterTag) : undefined, })}`, diff --git a/src/gprofiler/frontend/src/api/hooks/useGetFgData.jsx b/src/gprofiler/frontend/src/api/hooks/useGetFgData.jsx index ccd32c3e..45418db5 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetFgData.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetFgData.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext, useEffect } from 'react'; import { FgContext, SelectorsContext } from '../../states'; @@ -48,7 +48,7 @@ const useGetFgData = ({ disableLastWeekFetch = false }) => { const timeParams = getStartEndDateTimeFromSelection(timeSelection); const { data, loading, error } = useFetchWithRequest( { - url: `${DATA_URLS.GET_FLAMEGRAPH}?${stringify(_.assign({ ...timeParams }, fgParams))}`, + url: `${DATA_URLS.GET_FLAMEGRAPH}?${queryString.stringify(_.assign({ ...timeParams }, fgParams))}`, }, { refreshDeps: [selectedService, timeSelection, viewTruncated, JSON.stringify(activeFilterTag)], @@ -72,7 +72,7 @@ const useGetFgData = ({ disableLastWeekFetch = false }) => { run: lastWeekRun, } = useFetchWithRequest( { - url: `${DATA_URLS.GET_FLAMEGRAPH}?${stringify(_.assign({ ...weeklyParams }, fgParams))}`, + url: `${DATA_URLS.GET_FLAMEGRAPH}?${queryString.stringify(_.assign({ ...weeklyParams }, fgParams))}`, }, { manual: true } ); diff --git a/src/gprofiler/frontend/src/api/hooks/useGetFgMetrics.jsx b/src/gprofiler/frontend/src/api/hooks/useGetFgMetrics.jsx index a91f5c53..0f7f1419 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetFgMetrics.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetFgMetrics.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext, useState } from 'react'; import { isFilterTypeExist } from '@/components/filters/utils'; @@ -51,7 +51,7 @@ const useGetFgMetrics = ({ customTimeSelection, customService, disableCoreNodesR const { loading: metricsLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_METRICS + '?' + stringify(metricsParams), + url: DATA_URLS.GET_METRICS + '?' + queryString.stringify(metricsParams), }, { refreshDeps: [ @@ -73,7 +73,7 @@ const useGetFgMetrics = ({ customTimeSelection, customService, disableCoreNodesR const isHostNameFilterActive = isFilterTypeExist(FILTER_TYPES.HostName.value, activeFilterTag); const { loading: lastHtmlLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_LAST_HTML + '?' + stringify(metricsParams), + url: DATA_URLS.GET_LAST_HTML + '?' + queryString.stringify(metricsParams), }, { refreshDeps: [ @@ -99,12 +99,16 @@ const useGetFgMetrics = ({ customTimeSelection, customService, disableCoreNodesR serviceName: customService || selectedService, ignoreZeros: ignoreZeros, ...timeParams, - ...(!isServiceView ? { filter: activeFilterTag?.filter ? JSON.stringify(activeFilterTag) : undefined } : {}), + ...(!isServiceView + ? { + filter: activeFilterTag?.filter ? JSON.stringify(activeFilterTag) : undefined, + } + : {}), }; const { loading: coresNodesCountLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_NODES_AND_CORES + '?' + stringify(serviceAndTimeParams), + url: DATA_URLS.GET_NODES_AND_CORES + '?' + queryString.stringify(serviceAndTimeParams), }, { refreshDeps: [ @@ -129,7 +133,7 @@ const useGetFgMetrics = ({ customTimeSelection, customService, disableCoreNodesR const { loading: instanceTypeDataLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_INSTANCE_TYPE + '?' + stringify(serviceAndTimeParams), + url: DATA_URLS.GET_INSTANCE_TYPE + '?' + queryString.stringify(serviceAndTimeParams), }, { refreshDeps: [selectedService, customService, customTimeSelection ? customTimeSelection : timeSelection], diff --git a/src/gprofiler/frontend/src/api/hooks/useGetServiceMemoryAndCpu.jsx b/src/gprofiler/frontend/src/api/hooks/useGetServiceMemoryAndCpu.jsx index 117aa32c..a7f78906 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetServiceMemoryAndCpu.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetServiceMemoryAndCpu.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext } from 'react'; import { FilterTagsContext } from '../../states/filters/FiltersTagsContext'; @@ -46,7 +46,7 @@ const useGetServiceMemoryAndCpu = ({ resolution }) => { run: callMemoryAndCpuGraph, } = useFetchWithRequest( { - url: DATA_URLS.GET_GRAPH_METRICS + '?' + stringify(_.assign({ ...timeParams }, metricsParams)), + url: DATA_URLS.GET_GRAPH_METRICS + '?' + queryString.stringify(_.assign({ ...timeParams }, metricsParams)), }, { refreshDeps: [selectedService, timeSelection, resolution], diff --git a/src/gprofiler/frontend/src/api/hooks/useGetServiceNodesAndCores.jsx b/src/gprofiler/frontend/src/api/hooks/useGetServiceNodesAndCores.jsx index 6adbf1ff..a3473b20 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetServiceNodesAndCores.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetServiceNodesAndCores.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext } from 'react'; import { FilterTagsContext } from '@/states/filters/FiltersTagsContext'; @@ -50,7 +50,7 @@ const useGetServiceNodesAndCores = ({ resolution = null }) => { url: DATA_URLS.GET_NODES_AND_CORES_GRAPH_METRICS + '?' + - stringify(_.assign({ ...timeParams }, metricsParams)), + queryString.stringify(_.assign({ ...timeParams }, metricsParams)), }, { refreshDeps: [selectedService, timeSelection, resolution], diff --git a/src/gprofiler/frontend/src/api/hooks/useGetServiceSamples.jsx b/src/gprofiler/frontend/src/api/hooks/useGetServiceSamples.jsx index 0abb54aa..73f65318 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetServiceSamples.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetServiceSamples.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext } from 'react'; import { FilterTagsContext } from '../../states/filters/FiltersTagsContext'; @@ -43,7 +43,7 @@ const useGetServiceSamples = ({ resolution }) => { const timeParams = getStartEndDateTimeFromSelection(timeSelection); const { data: samplesData, loading: samplesLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_SAMPLES + '?' + stringify(_.assign({ ...timeParams }, metricsParams)), + url: DATA_URLS.GET_SAMPLES + '?' + queryString.stringify(_.assign({ ...timeParams }, metricsParams)), }, { refreshDeps: [selectedService, timeSelection, resolution], diff --git a/src/gprofiler/frontend/src/api/hooks/useGetTableFunctionCpuData.jsx b/src/gprofiler/frontend/src/api/hooks/useGetTableFunctionCpuData.jsx index 8a3cbcb6..3c374872 100644 --- a/src/gprofiler/frontend/src/api/hooks/useGetTableFunctionCpuData.jsx +++ b/src/gprofiler/frontend/src/api/hooks/useGetTableFunctionCpuData.jsx @@ -17,7 +17,7 @@ } import _ from 'lodash'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useContext, useState } from 'react'; import { FilterTagsContext } from '../../states/filters/FiltersTagsContext'; @@ -44,7 +44,10 @@ const useGetTableFunctionCpuData = ({ functionName, customService, customTimeSel const timeParams = getStartEndDateTimeFromSelection(customTimeSelection || timeSelection); const { data: functionCpuData, loading: functionCpuLoading } = useFetchWithRequest( { - url: DATA_URLS.GET_FUCNTION_CPU_GRAPH + '?' + stringify(_.assign({ ...timeParams }, metricsParams)), + url: + DATA_URLS.GET_FUCNTION_CPU_GRAPH + + '?' + + queryString.stringify(_.assign({ ...timeParams }, metricsParams)), }, { refreshDeps: [ @@ -61,7 +64,10 @@ const useGetTableFunctionCpuData = ({ functionName, customService, customTimeSel onSuccess: (result) => { setParsedData( result.map((cpuData) => { - return { cpu_percentage: cpuData.cpu_percentage * 100, time: cpuData.time }; + return { + cpu_percentage: cpuData.cpu_percentage * 100, + time: cpuData.time, + }; }) ); }, diff --git a/src/gprofiler/frontend/src/api/urls.js b/src/gprofiler/frontend/src/api/urls.js index 30de1528..65d035f1 100644 --- a/src/gprofiler/frontend/src/api/urls.js +++ b/src/gprofiler/frontend/src/api/urls.js @@ -16,7 +16,7 @@ */ } -import { stringify } from 'query-string'; +import queryString from 'query-string'; const API_PREFIX = '/api'; const FILETERS_PREFIX = '/v1/filters'; @@ -50,7 +50,7 @@ export const DATA_URLS = { // Filter endpoints FILTERS: `${API_PREFIX}${FILETERS_PREFIX}`, GET_FILTER_OPTIONS_VALUE: (filterType, params) => - `${API_PREFIX}${FILETERS_PREFIX}/tags/${filterType}?${stringify(params)}`, + `${API_PREFIX}${FILETERS_PREFIX}/tags/${filterType}?${queryString.stringify(params)}`, GET_FILTERS_FOR_SERVICE: (selectedService) => `${API_PREFIX}${FILETERS_PREFIX}/service/${selectedService}`, SNAPSHOT: `${API_PREFIX}/snapshots`, }; diff --git a/src/gprofiler/frontend/src/components/profiles/header/ProfilesActions.jsx b/src/gprofiler/frontend/src/components/profiles/header/ProfilesActions.jsx index 5df8b7ae..a0ce2aa8 100644 --- a/src/gprofiler/frontend/src/components/profiles/header/ProfilesActions.jsx +++ b/src/gprofiler/frontend/src/components/profiles/header/ProfilesActions.jsx @@ -18,7 +18,7 @@ import { ListItemIcon, ListItemText } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import { stringify } from 'query-string'; +import queryString from 'query-string'; import { useCallback, useContext, useRef, useState } from 'react'; import { useLocation } from 'react-router'; @@ -41,7 +41,7 @@ const DownloadLink = ({ serviceName, timeSelection, activeFilters, fileType, dow return ( { run: fetchFiles, } = useFetchWithRequest( { - url: `${DATA_URLS.GET_ADHOC_FLAMEGRAPHS}?${stringify({ + url: `${DATA_URLS.GET_ADHOC_FLAMEGRAPHS}?${queryString.stringify({ serviceName: selectedService, ...timeParams, filter: activeFilterTag?.filter ? JSON.stringify(activeFilterTag) : undefined, @@ -78,7 +78,7 @@ const AdhocProfilingView = () => { run: fetchFileContent, } = useFetchWithRequest( { - url: `${DATA_URLS.GET_ADHOC_FLAMEGRAPH_CONTENT}?${stringify({ + url: `${DATA_URLS.GET_ADHOC_FLAMEGRAPH_CONTENT}?${queryString.stringify({ serviceName: selectedService, filename: selectedFile?.filename, })}`, @@ -195,7 +195,10 @@ const AdhocProfilingView = () => { {filesData.map((file) => { const removedCellSx = file.removed - ? { textDecoration: 'line-through', color: 'text.disabled' } + ? { + textDecoration: 'line-through', + color: 'text.disabled', + } : {}; return ( { overflow: 'hidden', }}>