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)

Reply via email to