This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/atlas-2.6 by this push:
new 41ef7a649 ATLAS-5417: Atlas UI: Fix unescaped business metadata string
rendering in Classic UI basic search results table. (#769)
41ef7a649 is described below
commit 41ef7a6497013761d5b1aff63ce671f3ed0a6b0c
Author: Prasad Pawar <[email protected]>
AuthorDate: Mon Sep 28 16:33:36 2026 +0530
ATLAS-5417: Atlas UI: Fix unescaped business metadata string rendering in
Classic UI basic search results table. (#769)
Co-authored-by: Prasad Pawar <[email protected]>
(cherry-picked from the commit 89cf52a03471251bf6952a8dc5745b8c58b86928)
---
.../ShowMore/__tests__/ShowMoreText.test.tsx | 41 ++++++++++++++++++--
.../src/components/__tests__/HtmlRenderer.test.tsx | 32 ++++++++++++++++
.../components/__tests__/commonComponents.test.tsx | 29 ++++++++++++++
dashboard/src/utils/__tests__/Utils.test.ts | 44 +++++++++++++++++++++-
dashboardv2/public/js/utils/CommonViewFunction.js | 7 +++-
.../js/views/search/SearchResultLayoutView.js | 1 +
6 files changed, 148 insertions(+), 6 deletions(-)
diff --git a/dashboard/src/components/ShowMore/__tests__/ShowMoreText.test.tsx
b/dashboard/src/components/ShowMore/__tests__/ShowMoreText.test.tsx
index b07dea186..768757dec 100644
--- a/dashboard/src/components/ShowMore/__tests__/ShowMoreText.test.tsx
+++ b/dashboard/src/components/ShowMore/__tests__/ShowMoreText.test.tsx
@@ -26,10 +26,13 @@ import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import ShowMoreText from '../ShowMoreText'
-jest.mock('@utils/Utils', () => ({
- isEmpty: jest.fn((val: any) => !val || val === ''),
- sanitizeHtmlContent: jest.fn((html: string) => html)
-}))
+jest.mock('@utils/Utils', () => {
+ const actual = jest.requireActual('@utils/Utils')
+ return {
+ ...actual,
+ isEmpty: jest.fn((val: any) => !val || val === '')
+ }
+})
const { isEmpty } = require('@utils/Utils')
@@ -143,6 +146,36 @@ describe('ShowMoreText', () => {
expect(screen.getByText(text)).toBeTruthy()
})
+ it('sanitizes script tags when isHtml is true (negative)', () => {
+ const { container } = render(
+ <ShowMoreText
+ value='<script>alert("xss")</script><p>Safe</p>'
+ maxLength={160}
+ isHtml={true}
+ />
+ )
+
+ const htmlDiv = container.querySelector('.long-descriptions')
+ expect(htmlDiv?.querySelector('script')).toBeNull()
+ expect(htmlDiv?.querySelector('p')?.textContent).toBe('Safe')
+
expect(htmlDiv?.innerHTML.toLowerCase()).not.toContain('<script')
+ })
+
+ it('sanitizes img onerror when isHtml is true (negative)', () => {
+ const { container } = render(
+ <ShowMoreText
+ value='<img src=x
onerror="alert(1)"><em>Notes</em>'
+ maxLength={160}
+ isHtml={true}
+ />
+ )
+
+ const htmlDiv = container.querySelector('.long-descriptions')
+ expect(htmlDiv?.querySelector('img')).toBeNull()
+
expect(htmlDiv?.innerHTML.toLowerCase()).not.toContain('onerror')
+ expect(htmlDiv?.querySelector('em')?.textContent).toBe('Notes')
+ })
+
it('handles text exactly at maxLength', () => {
const exactText = 'A'.repeat(160)
render(<ShowMoreText value={exactText} maxLength={160} />)
diff --git a/dashboard/src/components/__tests__/HtmlRenderer.test.tsx
b/dashboard/src/components/__tests__/HtmlRenderer.test.tsx
index cc7dc92a3..e48b17346 100644
--- a/dashboard/src/components/__tests__/HtmlRenderer.test.tsx
+++ b/dashboard/src/components/__tests__/HtmlRenderer.test.tsx
@@ -50,4 +50,36 @@ describe('HtmlRenderer', () => {
expect(strong).toBeTruthy()
expect(strong?.textContent).toBe('Content')
})
+
+ it('strips script tags from malicious html (negative)', () => {
+ const { container } = render(
+ <HtmlRenderer
htmlString='<script>alert("xss")</script><p>Safe</p>' />
+ )
+
+ const root = container.querySelector('.html-content')
+ expect(root?.querySelector('script')).toBeNull()
+ expect(root?.querySelector('p')?.textContent).toBe('Safe')
+ expect(root?.innerHTML.toLowerCase()).not.toContain('<script')
+ })
+
+ it('strips img onerror handlers from malicious html (negative)', () => {
+ const { container } = render(
+ <HtmlRenderer htmlString='<img src=x
onerror="alert(1)"><em>Notes</em>' />
+ )
+
+ const root = container.querySelector('.html-content')
+ expect(root?.querySelector('img')).toBeNull()
+ expect(root?.innerHTML.toLowerCase()).not.toContain('onerror')
+ expect(root?.querySelector('em')?.textContent).toBe('Notes')
+ })
+
+ it('allows safe formatting tags (positive)', () => {
+ const { container } = render(
+ <HtmlRenderer htmlString='<p>Hello
<strong>world</strong></p>' />
+ )
+
+ const root = container.querySelector('.html-content')
+ expect(root?.querySelector('strong')?.textContent).toBe('world')
+ expect(root?.querySelector('p')?.textContent).toBe('Hello
world')
+ })
})
diff --git a/dashboard/src/components/__tests__/commonComponents.test.tsx
b/dashboard/src/components/__tests__/commonComponents.test.tsx
index 6d4ebcdf9..5e00dd6e7 100644
--- a/dashboard/src/components/__tests__/commonComponents.test.tsx
+++ b/dashboard/src/components/__tests__/commonComponents.test.tsx
@@ -438,6 +438,35 @@ describe('commonComponents', () => {
expect(container.textContent).toContain('text')
})
+ it('getValues renders malicious string as text without executing HTML
(negative)', () => {
+ const xssPayload = '<img src=x onerror="alert(1)">'
+ const result = getValues(
+ { getValue: () => xssPayload },
+ {},
+ { name: 'bm1.notes' }
+ )
+ const { container } = render(<>{result}</>)
+
+ expect(container.querySelector('img')).toBeNull()
+ expect(container.textContent).toContain('<img')
+ // React escapes markup — payload visible as text, not parsed
as HTML
+ expect(container.innerHTML).toContain('<img')
+ expect(container.innerHTML).not.toMatch(/<img[\s>]/)
+ })
+
+ it('getValues renders plain metadata string unchanged (positive)', ()
=> {
+ const plainText = 'Sales & Marketing notes'
+ const result = getValues(
+ { getValue: () => plainText },
+ {},
+ { name: 'bm1.notes' }
+ )
+ const { container } = render(<>{result}</>)
+
+ expect(container.textContent).toContain(plainText)
+ expect(container.querySelector('img')).toBeNull()
+ })
+
it('getValues renders direct string value when properties param is
set', () => {
const result = getValues(
'audit-label-value',
diff --git a/dashboard/src/utils/__tests__/Utils.test.ts
b/dashboard/src/utils/__tests__/Utils.test.ts
index 095fa6874..0af93391e 100644
--- a/dashboard/src/utils/__tests__/Utils.test.ts
+++ b/dashboard/src/utils/__tests__/Utils.test.ts
@@ -904,11 +904,53 @@ describe('Utils', () => {
});
describe('sanitizeHtmlContent', () => {
- it('should sanitize HTML content', () => {
+ it('should strip script tags and keep allowed markup
(positive)', () => {
const html = '<script>alert("xss")</script><p>Safe
content</p>';
const result = sanitizeHtmlContent(html);
expect(result).not.toContain('<script>');
+ expect(result).not.toContain('alert');
expect(result).toContain('<p>');
+ expect(result).toContain('Safe content');
+ });
+
+ it('should strip img onerror event handlers (negative)', () => {
+ const html = '<img src=x
onerror="alert(1)"><p>Notes</p>';
+ const result = sanitizeHtmlContent(html);
+ expect(result.toLowerCase()).not.toContain('onerror');
+ expect(result).not.toContain('<img');
+ expect(result).toContain('<p>');
+ });
+
+ it('should strip iframe tags (negative)', () => {
+ const html = '<iframe
src="javascript:alert(1)"></iframe><em>text</em>';
+ const result = sanitizeHtmlContent(html);
+ expect(result).not.toContain('<iframe');
+ expect(result).toContain('<em>');
+ });
+
+ it('should strip javascript: href schemes from anchors
(negative)', () => {
+ const html = '<a href="javascript:alert(1)">click</a>';
+ const result = sanitizeHtmlContent(html);
+
expect(result.toLowerCase()).not.toContain('javascript:');
+ });
+
+ it('should allow safe anchor href with https (positive)', () =>
{
+ const html = '<a href="https://example.com">link</a>';
+ const result = sanitizeHtmlContent(html);
+ expect(result).toContain('href="https://example.com"');
+ expect(result).toContain('link');
+ });
+
+ it('should allow basic formatting tags (positive)', () => {
+ const html = '<strong>Bold</strong> and
<em>italic</em>';
+ const result = sanitizeHtmlContent(html);
+ expect(result).toContain('<strong>');
+ expect(result).toContain('<em>');
+ });
+
+ it('should return empty string for nullish input (negative)',
() => {
+ expect(sanitizeHtmlContent(null)).toBe('');
+ expect(sanitizeHtmlContent(undefined)).toBe('');
});
});
diff --git a/dashboardv2/public/js/utils/CommonViewFunction.js
b/dashboardv2/public/js/utils/CommonViewFunction.js
index 713e82617..a428646e8 100644
--- a/dashboardv2/public/js/utils/CommonViewFunction.js
+++ b/dashboardv2/public/js/utils/CommonViewFunction.js
@@ -127,7 +127,11 @@ define(['require', 'utils/Utils', 'modules/Modal',
'utils/Messages', 'utils/Enum
newVal = numberFormat(newVal);
}
}
- newVal = (options && options.isEditorValue) ? newVal :
_.escape(newVal);
+ // Business-metadata string attrs may contain rich
text; sanitize instead of
+ // skipping escape (stored XSS via REST API + search
table HtmlCell).
+ newVal = (options && options.isEditorValue)
+ ? Utils.sanitizeHtmlContent({ data: newVal })
+ : _.escape(newVal);
return getHighlightedString(newVal);
}
} else {
@@ -342,6 +346,7 @@ define(['require', 'utils/Utils', 'modules/Modal',
'utils/Messages', 'utils/Enum
}
table += '<tr class="' + appendClass + '"><td>' +
(_.escape(key) + listCount) + '</td><td>' + htmlTag + '</td></tr>';
} else {
+ // val is sanitized (isEditorValue) or entity-escaped via
getValue() above.
table += '<span>' + val + '</span>';
}
diff --git a/dashboardv2/public/js/views/search/SearchResultLayoutView.js
b/dashboardv2/public/js/views/search/SearchResultLayoutView.js
index 160736971..4b90db170 100644
--- a/dashboardv2/public/js/views/search/SearchResultLayoutView.js
+++ b/dashboardv2/public/js/views/search/SearchResultLayoutView.js
@@ -717,6 +717,7 @@ define(['require',
);
}
},
+ // Marks business-metadata string columns so search results
sanitize (not raw HTML).
checkIsEditorValue: function(key) {
var isEditorKey = false;
if (this.businessMetadataAttributes.length) {