You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/05/30 06:07:02 UTC

(superset) branch fix/xss-dom-and-cleartext-logging created (now 313135540b9)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a change to branch fix/xss-dom-and-cleartext-logging
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 313135540b9 fix(security): sanitize URL sinks and trim sensitive log fields

This branch includes the following new commits:

     new 313135540b9 fix(security): sanitize URL sinks and trim sensitive log fields

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fix(security): sanitize URL sinks and trim sensitive log fields

Posted by ru...@apache.org.
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 <no...@anthropic.com>
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 <no...@anthropic.com>
---
 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)