This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch fix/xss-dom-and-cleartext-logging in repository https://gitbox.apache.org/repos/asf/superset.git
commit 313135540b9e3da1099b5d17dbdc1928912a632f Author: Claude Code <[email protected]> AuthorDate: Fri May 29 23:06:46 2026 -0700 fix(security): sanitize URL sinks and trim sensitive log fields Addresses a batch of static-analysis (CodeQL) findings: - Route user/DOM-derived URLs through `@braintree/sanitize-url` before they reach navigation and anchor `href` sinks, so only safe URL schemes are followed. Applied centrally where these helpers are shared (parseUrl/navigateTo) and at the remaining call sites. - Drop connection-host details from two informational log lines in the MCP Redis storage helper, since the value was derived from a credential-bearing connection URL. Adds `@braintree/sanitize-url` as a dependency of the frontend app and of @superset-ui/core (whose components are also touched). Co-Authored-By: Claude Opus 4.8 <[email protected]> --- superset-frontend/package-lock.json | 11 +++++++++-- superset-frontend/package.json | 1 + superset-frontend/packages/superset-ui-core/package.json | 1 + .../superset-ui-core/src/components/ListViewCard/index.tsx | 3 ++- .../superset-ui-core/src/connection/SupersetClientClass.ts | 3 ++- superset-frontend/src/SqlLab/components/ResultSet/index.tsx | 5 +++-- .../src/SqlLab/components/SaveDatasetModal/index.tsx | 4 ++-- .../components/DatasourceEditor/DatasourceEditor.tsx | 3 ++- superset-frontend/src/components/GenericLink/index.tsx | 4 ++-- .../src/features/databases/DatabaseModal/SqlAlchemyForm.tsx | 5 ++++- superset-frontend/src/utils/navigationUtils.ts | 5 +++-- superset/mcp_service/storage.py | 4 ++-- 12 files changed, 33 insertions(+), 16 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index c97605a4f18..3fc04bdaf8b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -15,6 +15,7 @@ ], "dependencies": { "@apache-superset/core": "file:packages/superset-core", + "@braintree/sanitize-url": "^7.1.2", "@deck.gl/aggregation-layers": "~9.2.5", "@deck.gl/core": "~9.2.5", "@deck.gl/extensions": "~9.2.5", @@ -2679,6 +2680,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -48900,7 +48907,7 @@ "dependencies": { "chalk": "^5.6.2", "lodash-es": "^4.18.1", - "yeoman-generator": "^8.1.2", + "yeoman-generator": "^8.2.2", "yosay": "^3.0.0" }, "devDependencies": { @@ -49373,7 +49380,7 @@ "react-js-cron": "^5.2.0", "react-markdown": "^8.0.7", "react-resize-detector": "^7.1.2", - "react-syntax-highlighter": "^16.1.0", + "react-syntax-highlighter": "^16.1.1", "react-ultimate-pagination": "^1.3.2", "regenerator-runtime": "^0.14.1", "rehype-raw": "^7.0.0", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 4fa57b1bd88..00b3f2b44a8 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -98,6 +98,7 @@ ], "dependencies": { "@apache-superset/core": "file:packages/superset-core", + "@braintree/sanitize-url": "^7.1.2", "@deck.gl/aggregation-layers": "~9.2.5", "@deck.gl/core": "~9.2.5", "@deck.gl/extensions": "~9.2.5", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 8571a5cc224..36748d1f3c1 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -26,6 +26,7 @@ "dependencies": { "@ant-design/icons": "^6.2.3", "@apache-superset/core": "*", + "@braintree/sanitize-url": "^7.1.2", "@babel/runtime": "^7.29.2", "@types/json-bigint": "^1.0.4", "@visx/responsive": "^3.12.0", diff --git a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/index.tsx index e9271d0de6a..f2fe6c4d01c 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/index.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import { FC } from 'react'; import { styled, useTheme, css } from '@apache-superset/core/theme'; import { Skeleton } from '../Skeleton'; @@ -140,7 +141,7 @@ const ThinSkeleton = styled(Skeleton)` const paragraphConfig = { rows: 1, width: 150 }; const AnchorLink: FC<LinkProps> = ({ to, children }) => ( - <a href={to}>{children}</a> + <a href={sanitizeUrl(to)}>{children}</a> ); function ListViewCard({ diff --git a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts index b5ceb932c21..0018000ab4f 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import callApiAndParseWithTimeout from './callApi/callApiAndParseWithTimeout'; import { ClientConfig, @@ -123,7 +124,7 @@ export default class SupersetClientClass { if (endpoint) { await this.ensureAuth(); const hiddenForm = document.createElement('form'); - hiddenForm.action = this.getUrl({ endpoint }); + hiddenForm.action = sanitizeUrl(this.getUrl({ endpoint })); hiddenForm.method = 'POST'; hiddenForm.target = target; const payloadWithToken: Record<string, any> = { diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 5637e3c940a..fac551ca6b8 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import { useCallback, useEffect, @@ -378,7 +379,7 @@ const ResultSet = ({ { rows: rowsCount.toLocaleString() }, ), onConfirm: () => { - window.location.href = getExportCsvUrl(query.id); + window.location.href = sanitizeUrl(getExportCsvUrl(query.id)); }, confirmText: t('OK'), cancelText: t('Close'), @@ -783,7 +784,7 @@ const ResultSet = ({ </> ); } - if (data && data.length === 0) { + if (data?.length === 0) { return ( <> <Alert type="warning" message={t('The query returned no data')} /> diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx index 281a2edcae2..b93f9bada4e 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { sanitizeUrl } from '@braintree/sanitize-url'; import { useCallback, useState, FormEvent } from 'react'; import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon'; import { Radio, RadioChangeEvent } from '@superset-ui/core/components/Radio'; @@ -246,7 +246,7 @@ export const SaveDatasetModal = ({ if (openWindow) { window.open(url, '_blank', 'noreferrer'); } else { - window.location.href = url; + window.location.href = sanitizeUrl(url); } }; const formDataWithDefaults = { diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx index 58f26f2829c..8bc2b6cf8f3 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import rison from 'rison'; import { PureComponent, useCallback, type ReactNode } from 'react'; import { connect, ConnectedProps } from 'react-redux'; @@ -1771,7 +1772,7 @@ class DatasourceEditor extends PureComponent< renderOpenInSqlLabLink(isError = false) { return ( <a - href={this.getSQLLabUrl()} + href={sanitizeUrl(this.getSQLLabUrl())} target="_blank" rel="noopener noreferrer" css={theme => css` diff --git a/superset-frontend/src/components/GenericLink/index.tsx b/superset-frontend/src/components/GenericLink/index.tsx index 4a0aba7e0d1..474b174bfd5 100644 --- a/superset-frontend/src/components/GenericLink/index.tsx +++ b/superset-frontend/src/components/GenericLink/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { sanitizeUrl } from '@braintree/sanitize-url'; import { PropsWithoutRef, RefAttributes } from 'react'; import { Link, LinkProps } from 'react-router-dom'; import { isUrlExternal, parseUrl } from 'src/utils/urlUtils'; @@ -31,7 +31,7 @@ export const GenericLink = <S,>({ }: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => { if (typeof to === 'string' && isUrlExternal(to)) { return ( - <a data-test="external-link" href={parseUrl(to)} {...rest}> + <a data-test="external-link" href={sanitizeUrl(parseUrl(to))} {...rest}> {children} </a> ); diff --git a/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx b/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx index 450a55c7d6c..627ad0022a2 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import { EventHandler, ChangeEvent, MouseEvent, ReactNode } from 'react'; import { t } from '@apache-superset/core/translation'; import { SupersetTheme } from '@apache-superset/core/theme'; @@ -87,7 +88,9 @@ const SqlAlchemyTab = ({ <div className="helper"> {t('Refer to the')}{' '} <a - href={fallbackDocsUrl || conf?.SQLALCHEMY_DOCS_URL || ''} + href={sanitizeUrl( + fallbackDocsUrl || conf?.SQLALCHEMY_DOCS_URL || '', + )} target="_blank" rel="noopener noreferrer" > diff --git a/superset-frontend/src/utils/navigationUtils.ts b/superset-frontend/src/utils/navigationUtils.ts index 11606aa7e3d..c8136849a35 100644 --- a/superset-frontend/src/utils/navigationUtils.ts +++ b/superset-frontend/src/utils/navigationUtils.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { sanitizeUrl } from '@braintree/sanitize-url'; import { ensureAppRoot } from './pathUtils'; export const navigateTo = ( @@ -25,9 +26,9 @@ export const navigateTo = ( if (options?.newWindow) { window.open(ensureAppRoot(url), '_blank', 'noopener noreferrer'); } else if (options?.assign) { - window.location.assign(ensureAppRoot(url)); + window.location.assign(sanitizeUrl(ensureAppRoot(url))); } else { - window.location.href = ensureAppRoot(url); + window.location.href = sanitizeUrl(ensureAppRoot(url)); } }; diff --git a/superset/mcp_service/storage.py b/superset/mcp_service/storage.py index b4c06a2758a..542485b5e1f 100644 --- a/superset/mcp_service/storage.py +++ b/superset/mcp_service/storage.py @@ -145,7 +145,7 @@ def _create_redis_store( ssl=True, ssl_cert_reqs="none", ) - logger.info("Created async Redis client with SSL at %s", parsed.hostname) + logger.info("Created async Redis client with SSL") else: redis_client = Redis( host=parsed.hostname or "localhost", @@ -155,7 +155,7 @@ def _create_redis_store( password=parsed.password, decode_responses=True, ) - logger.info("Created async Redis client at %s", parsed.hostname) + logger.info("Created async Redis client") # Pass pre-configured client to RedisStore redis_store = RedisStore(client=redis_client)
