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('&lt;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) {

Reply via email to