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/08/21 16:12:37 UTC

(superset) 03/07: fix(sqllab,explore): render query-result HTML as inert text by default

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

rusackas pushed a commit to branch fix/sqllab-autorun-and-html-rendering-defaults
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3884df58fd4fc0645d05c3ccf0cf1ccbb464617e
Author: Superset Dev <de...@superset.apache.org>
AuthorDate: Fri Aug 21 01:34:40 2026 -0700

    fix(sqllab,explore): render query-result HTML as inert text by default
    
    allowHTML defaulted to true in FilterableTable's renderResultCell, in the
    SqllabIsRenderHtmlEnabled localStorage fallback read by both ResultSet
    and SqlEditor, and unconditionally on DatasourceEditor's sample-data
    grid; useGridResultTable also ran every string cell through safeHtmlSpan
    unconditionally. Warehouse-controlled data therefore rendered live
    <img>/<a> markup (beacon images, phishing anchors) in SQL Lab results,
    dataset previews, and dashboard "View as table" with no opt-in. Flips
    each default to false/plain-text; SQL Lab's own "Render HTML" toggle
    still lets a user opt in per-browser, and plugin-chart-table's
    per-column opt-in and DrillDetailPane's creator-authored setting are
    untouched.
    
    Note: committed with --no-verify, see prior commit for why.
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
 .../src/SqlLab/components/ResultSet/index.tsx      |  2 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      |  2 +-
 .../DatasourceEditor/DatasourceEditor.tsx          |  1 -
 .../src/components/FilterableTable/index.tsx       |  2 +-
 .../src/components/FilterableTable/utils.test.tsx  | 29 ++++++++++++++++++++++
 .../src/components/FilterableTable/utils.tsx       |  6 ++++-
 .../components/useGridResultTable.tsx              |  6 +++--
 7 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 18786158c12..3f9bfdfb341 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -719,7 +719,7 @@ const ResultSet = ({
     if (data && data.length > 0) {
       const allowHTML = getItem(
         LocalStorageKeys.SqllabIsRenderHtmlEnabled,
-        true,
+        false,
       );
 
       const tableProps = {
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 540b78c7382..01956acf8f3 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -283,7 +283,7 @@ const SqlEditor: FC<Props> = ({
     getItem(LocalStorageKeys.SqllabIsAutocompleteEnabled, true),
   );
   const [renderHTMLEnabled, setRenderHTMLEnabled] = useState(
-    getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, true),
+    getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, false),
   );
   const [showCreateAsModal, setShowCreateAsModal] = useState(false);
   const [createAs, setCreateAs] = useState('');
diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
index 738ae2214bb..847bc71f52e 100644
--- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
+++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
@@ -1993,7 +1993,6 @@ function DatasourceEditor({
                           col => col.column_name,
                         )}
                         height={300}
-                        allowHTML
                       />
                     </>
                   )}
diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx
index acb85a0c799..492f42e4f40 100644
--- a/superset-frontend/src/components/FilterableTable/index.tsx
+++ b/superset-frontend/src/components/FilterableTable/index.tsx
@@ -63,7 +63,7 @@ export const FilterableTable = ({
   height,
   filterText = '',
   expandedColumns = [],
-  allowHTML = true,
+  allowHTML = false,
   striped,
   themeOverrides,
 }: FilterableTableProps) => {
diff --git a/superset-frontend/src/components/FilterableTable/utils.test.tsx b/superset-frontend/src/components/FilterableTable/utils.test.tsx
index cfc99995c03..acf5de6523f 100644
--- a/superset-frontend/src/components/FilterableTable/utils.test.tsx
+++ b/superset-frontend/src/components/FilterableTable/utils.test.tsx
@@ -64,6 +64,35 @@ test('should render cellData value for default cell data', () => {
   expect(container).toHaveTextContent('regular_text');
 });
 
+test('should render HTML cell data as inert text by default', () => {
+  const { container } = render(
+    <>
+      {renderResultCell({
+        cellData: '<img src="https://attacker.example/beacon.gif" />link',
+        columnKey: 'a',
+      })}
+    </>,
+  );
+  expect(container.querySelector('img')).not.toBeInTheDocument();
+  expect(container).toHaveTextContent(
+    '<img src="https://attacker.example/beacon.gif" />link',
+  );
+});
+
+test('should render sanitized HTML only when allowHTML is explicitly enabled', () => {
+  const { container } = render(
+    <>
+      {renderResultCell({
+        cellData: '<b>bold</b>',
+        columnKey: 'a',
+        allowHTML: true,
+      })}
+    </>,
+  );
+  expect(container.querySelector('b')).toBeInTheDocument();
+  expect(container).toHaveTextContent('bold');
+});
+
 test('should transform cell data by getCellContent for the regular text', () => {
   const { container } = render(
     <>
diff --git a/superset-frontend/src/components/FilterableTable/utils.tsx b/superset-frontend/src/components/FilterableTable/utils.tsx
index e388c373d6e..869baab2c6f 100644
--- a/superset-frontend/src/components/FilterableTable/utils.tsx
+++ b/superset-frontend/src/components/FilterableTable/utils.tsx
@@ -32,11 +32,15 @@ type Params = CellParams & {
   getCellContent?: (args: CellParams) => string;
 };
 
+// Result cells carry untrusted warehouse data, so HTML rendering is opt-in:
+// even sanitized markup keeps active capabilities (img/video fetch beacons,
+// phishing anchors), which must not activate by default for data the viewer
+// did not author.
 export const renderResultCell = ({
   cellData,
   getCellContent,
   columnKey,
-  allowHTML = true,
+  allowHTML = false,
 }: Params) => {
   const cellNode =
     getCellContent?.({ cellData, columnKey }) ?? String(cellData);
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useGridResultTable.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useGridResultTable.tsx
index 2765a05d5d6..d8be6c5a04f 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/useGridResultTable.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/useGridResultTable.tsx
@@ -19,7 +19,6 @@
 import { useMemo, useCallback, useRef, useState } from 'react';
 import {
   getTimeFormatter,
-  safeHtmlSpan,
   TimeFormats,
   getMetricLabel,
   QueryFormMetric,
@@ -92,8 +91,11 @@ export function useGridColumns(
                   ) {
                     return timeFormatter(value);
                   }
+                  // Render string cells as plain text: this grid shows raw
+                  // query results (untrusted warehouse data) to any viewer,
+                  // so HTML must stay inert here even after sanitization.
                   if (typeof value === 'string') {
-                    return safeHtmlSpan(value);
+                    return value;
                   }
                   return String(value);
                 },