From 7edffaa148fe1b48282c28aeba516fa425b5df97 Mon Sep 17 00:00:00 2001 From: dganesh05 Date: Sun, 2 Nov 2025 17:46:35 -0500 Subject: [PATCH] feat(html): add escapeHtml function to sanitize HTML entities and update tests - Implemented escapeHtml function to escape HTML entities in strings. - Added unit tests for escapeHtml to ensure correct functionality. - Updated FilterableTable to use escapeHtml for rendering string data safely. - Modified .gitignore to include .cursor/ files. - Updated package-lock.json to add peerDependencies for eslint-plugin-icons. --- .gitignore | 2 + superset-frontend/package-lock.json | 5 +- .../superset-ui-core/src/utils/html.test.tsx | 47 +++++++++++++++++++ .../superset-ui-core/src/utils/html.tsx | 18 +++++++ .../src/components/FilterableTable/utils.tsx | 23 ++++++++- 5 files changed, 92 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b3ef8b6db115..1600a06ea3cc 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ docker/*local* test-report.html superset/static/stats/statistics.html .aider* + +.cursor/ diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 793ca61edfdd..48240a0b4888 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -321,7 +321,10 @@ "eslint-rules/eslint-plugin-icons": { "version": "1.0.0", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peerDependencies": { + "eslint": ">=0.8.0" + } }, "eslint-rules/eslint-plugin-theme-colors": { "version": "1.0.0", 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 3f38e8c9c162..3c1e761d5632 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 @@ -24,6 +24,7 @@ import { removeHTMLTags, isJsonString, getParagraphContents, + escapeHtml, } from './html'; describe('sanitizeHtml', () => { @@ -189,3 +190,49 @@ describe('getParagraphContents', () => { }); }); }); + +describe('escapeHtml', () => { + test('should escape angle brackets', () => { + const input = '
test
'; + const output = escapeHtml(input); + expect(output).toBe('<div>test</div>'); + }); + + test('should escape ampersand', () => { + const input = 'A & B'; + const output = escapeHtml(input); + expect(output).toBe('A & B'); + }); + + test('should escape quotes', () => { + const input = 'Say "hello" and \'hi\''; + const output = escapeHtml(input); + expect(output).toBe('Say "hello" and 'hi''); + }); + + test('should escape all HTML entities in complex string', () => { + const input = ''; + const output = escapeHtml(input); + expect(output).toBe( + '<script>alert("XSS & more")</script>', + ); + }); + + test('should return plain text unchanged', () => { + const input = 'Just plain text'; + const output = escapeHtml(input); + expect(output).toBe('Just plain text'); + }); + + test('should handle strings with comparison operators', () => { + const input = 'a <= 10 and b > 10'; + const output = escapeHtml(input); + expect(output).toBe('a <= 10 and b > 10'); + }); + + test('should handle empty string', () => { + const input = ''; + const output = escapeHtml(input); + expect(output).toBe(''); + }); +}); 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 723c0dc3d968..ab283dbe3968 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx @@ -87,6 +87,24 @@ export function removeHTMLTags(str: string): string { return str.replace(/<[^>]*>/g, ''); } +/** + * Escapes HTML entities in a string to prevent it from being interpreted as HTML. + * This is useful for displaying strings containing angle brackets as plain text. + * + * @param str - The string to escape + * @returns The escaped string with HTML entities + */ +export function escapeHtml(str: string): string { + const escapeMap: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return str.replace(/[&<>"']/g, match => escapeMap[match]); +} + export function isJsonString(str: string): boolean { try { JSON.parse(str); diff --git a/superset-frontend/src/components/FilterableTable/utils.tsx b/superset-frontend/src/components/FilterableTable/utils.tsx index a4e4a4c84450..30aa725138a6 100644 --- a/superset-frontend/src/components/FilterableTable/utils.tsx +++ b/superset-frontend/src/components/FilterableTable/utils.tsx @@ -17,7 +17,7 @@ * under the License. */ import { JsonModal, safeJsonObjectParse } from 'src/components/JsonModal'; -import { t, safeHtmlSpan } from '@superset-ui/core'; +import { t, safeHtmlSpan, escapeHtml, isProbablyHTML } from '@superset-ui/core'; import { NULL_STRING, CellDataType } from './useCellContentParser'; type CellParams = { @@ -51,7 +51,26 @@ export const renderResultCell = ({ /> ); } - if (allowHTML && typeof cellData === 'string') { + if (typeof cellData === 'string') { + // For SQL Lab query results, always display strings as text (not as HTML) + // Escape HTML entities so strings like '
test
' display correctly + if (!allowHTML) { + // When HTML is not allowed, always escape entities + // Wrap in span and use dangerouslySetInnerHTML to ensure proper rendering + return ( + + ); + } + // When HTML is allowed, check if it looks like HTML + // If it does, escape it to display as text (SQL results should show actual values) + // If it doesn't, use safeHtmlSpan for potential HTML rendering + if (isProbablyHTML(cellNode)) { + // Escape HTML-like strings to display them as literal text + // Use dangerouslySetInnerHTML so the browser decodes the entities correctly + return ( + + ); + } return safeHtmlSpan(cellNode); } return cellNode;