From edda9d1bbbc45364d0781576ab30921d8367dfba Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 21 Aug 2026 12:31:08 -0700 Subject: [PATCH] fix(frontend): tighten SQL Lab autorun scoping and HTML-rendering defaults (#43398) Co-authored-by: Superset Dev Co-authored-by: Claude Sonnet 5 --- .../superset-ui-core/src/utils/html.test.tsx | 37 +++++++++++ .../superset-ui-core/src/utils/html.tsx | 27 ++++++-- .../src/renderers/NumericCellRenderer.tsx | 12 +++- .../test/NumericCellRenderer.test.tsx | 59 +++++++++++++++++ .../src/SqlLab/actions/sqlLab.ts | 12 +++- .../src/SqlLab/components/ResultSet/index.tsx | 2 +- .../src/SqlLab/components/SqlEditor/index.tsx | 2 +- .../DatasourceEditor/DatasourceEditor.tsx | 1 - .../src/components/FilterableTable/index.tsx | 2 +- .../components/FilterableTable/utils.test.tsx | 29 ++++++++ .../src/components/FilterableTable/utils.tsx | 6 +- .../components/useGridResultTable.tsx | 6 +- .../ColorBreakpointOption.test.tsx | 25 +++++++ .../ColorBreakpointOption.tsx | 17 ++++- .../useExploreAdditionalActionsMenu/index.tsx | 43 +++++++++--- .../useExploreAdditionalActionsMenu.test.tsx | 40 +++++++++++ .../src/pages/SqlLab/LocationContext.test.tsx | 66 +++++++++++++++++++ .../src/pages/SqlLab/LocationContext.tsx | 6 +- 18 files changed, 366 insertions(+), 26 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-ag-grid-table/test/NumericCellRenderer.test.tsx create mode 100644 superset-frontend/src/pages/SqlLab/LocationContext.test.tsx diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx index d2928d66d0b..36b63091a91 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx @@ -123,6 +123,25 @@ describe('isProbablyHTML', () => { expect(isProbablyHTML('')).toBe(true); expect(isProbablyHTML('')).toBe(true); }); + + test('should return true for script-capable and foreign-content tags', () => { + expect(isProbablyHTML('')).toBe(true); + expect(isProbablyHTML('x')).toBe(true); + expect( + isProbablyHTML('
x
'), + ).toBe(true); + expect(isProbablyHTML('x')).toBe(true); + expect(isProbablyHTML('')).toBe(true); + expect(isProbablyHTML('')).toBe(true); + expect(isProbablyHTML('x')).toBe(true); + expect(isProbablyHTML('')).toBe(true); + expect(isProbablyHTML('x')).toBe(true); + }); + + test('should return true for elements that parse into document.head', () => { + expect(isProbablyHTML('')).toBe(true); + expect(isProbablyHTML('injected')).toBe(true); + }); }); describe('sanitizeHtmlIfNeeded', () => { @@ -137,6 +156,24 @@ describe('sanitizeHtmlIfNeeded', () => { const sanitizedString = sanitizeHtmlIfNeeded(plainText); expect(sanitizedString).toEqual(plainText); }); + + test('should sanitize svg/details/style payloads instead of passing them through', () => { + const svgPayload = ''; + const sanitizedSvg = sanitizeHtmlIfNeeded(svgPayload); + expect(sanitizedSvg).not.toContain('x'); + + const stylePayload = ''; + expect(sanitizeHtmlIfNeeded(stylePayload)).not.toContain(' { diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx index 8e5e8e48297..e3ecfc7fd92 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx @@ -154,6 +154,20 @@ const KNOWN_HTML_TAGS = new Set([ 'html', 'head', 'body', + // Script-capable elements and foreign-content roots (SVG/MathML). These + // must be classified as HTML so that downstream sanitization is applied; + // omitting them makes the heuristic fail open — payloads such as + // `` or `
` would be classified + // "not HTML" and returned verbatim by sanitizeHtmlIfNeeded. + 'svg', + 'math', + 'details', + 'summary', + 'object', + 'embed', + 'marquee', + 'template', + 'dialog', ]); const HTML_TAG_PATTERN = new RegExp( @@ -183,10 +197,15 @@ export function isProbablyHTML(text: string) { const parser = new DOMParser(); const doc = parser.parseFromString(cleanedStr, 'text/html'); - // Check if parsing created actual HTML elements (not just text nodes) - const elements = Array.from(doc.body.childNodes).filter( - node => node.nodeType === 1, - ) as Element[]; + // Check if parsing created actual HTML elements (not just text nodes). + // Some elements (e.g.