diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index af4ba32177a..ed9a025c056 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -2,7 +2,6 @@ import './LogDetails.styles.scss'; import { Color, Spacing } from '@signozhq/design-tokens'; -import Convert from 'ansi-to-html'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import { RadioChangeEvent } from 'antd/lib'; import cx from 'classnames'; @@ -17,12 +16,10 @@ import JSONView from 'container/LogDetailedView/JsonView'; import Overview from 'container/LogDetailedView/Overview'; import { aggregateAttributesResourcesToString, - escapeHtml, + getSanitizedLogBody, removeEscapeCharacters, - unescapeString, } from 'container/LogDetailedView/utils'; import { useOptionsMenu } from 'container/OptionsMenu'; -import dompurify from 'dompurify'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -46,14 +43,11 @@ import { AppState } from 'store/reducers'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; -import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants'; import { LogDetailProps } from './LogDetail.interfaces'; import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper'; -const convert = new Convert(); - function LogDetail({ log, onClose, @@ -118,11 +112,7 @@ function LogDetail({ const htmlBody = useMemo( () => ({ - __html: convert.toHtml( - dompurify.sanitize(unescapeString(escapeHtml(log?.body || '')), { - FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], - }), - ), + __html: getSanitizedLogBody(log?.body || '', { shouldEscapeHtml: true }), }), [log?.body], ); diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index 8526ce0be8e..bdc8b2e77f0 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -1,15 +1,13 @@ import './ListLogView.styles.scss'; import { blue } from '@ant-design/colors'; -import Convert from 'ansi-to-html'; import { Typography } from 'antd'; import cx from 'classnames'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils'; +import { getSanitizedLogBody } from 'container/LogDetailedView/utils'; import { FontSize } from 'container/OptionsMenu/types'; -import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -20,7 +18,6 @@ import { useCallback, useMemo, useState } from 'react'; // interfaces import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; -import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; // components import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC'; @@ -37,8 +34,6 @@ import { } from './styles'; import { isValidLogField } from './util'; -const convert = new Convert(); - interface LogFieldProps { fieldKey: string; fieldValue: string; @@ -57,11 +52,7 @@ function LogGeneralField({ }: LogFieldProps): JSX.Element { const html = useMemo( () => ({ - __html: convert.toHtml( - dompurify.sanitize(unescapeString(escapeHtml(fieldValue)), { - FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], - }), - ), + __html: getSanitizedLogBody(fieldValue, { shouldEscapeHtml: true }), }), [fieldValue], ); diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 9a305904a67..4ad7329f835 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -1,13 +1,11 @@ import './RawLogView.styles.scss'; -import Convert from 'ansi-to-html'; import { DrawerProps } from 'antd'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils'; +import { getSanitizedLogBody } from 'container/LogDetailedView/utils'; import LogsExplorerContext from 'container/LogsExplorerContext'; -import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; // hooks @@ -23,7 +21,6 @@ import { useMemo, useState, } from 'react'; -import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons'; import LogStateIndicator from '../LogStateIndicator/LogStateIndicator'; @@ -32,8 +29,6 @@ import { getLogIndicatorType } from '../LogStateIndicator/utils'; import { RawLogContent, RawLogViewContainer } from './styles'; import { RawLogViewProps } from './types'; -const convert = new Convert(); - function RawLogView({ isActiveLog, isReadOnly, @@ -176,11 +171,7 @@ function RawLogView({ const html = useMemo( () => ({ - __html: convert.toHtml( - dompurify.sanitize(unescapeString(escapeHtml(text)), { - FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], - }), - ), + __html: getSanitizedLogBody(text, { shouldEscapeHtml: true }), }), [text], ); diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index 8b97f6cfd24..0551ff479f3 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -1,17 +1,14 @@ import './useTableView.styles.scss'; -import Convert from 'ansi-to-html'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import cx from 'classnames'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { unescapeString } from 'container/LogDetailedView/utils'; -import dompurify from 'dompurify'; +import { getSanitizedLogBody } from 'container/LogDetailedView/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { FlatLogData } from 'lib/logs/flatLogData'; import { useTimezone } from 'providers/Timezone'; import { useMemo } from 'react'; -import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; import LogStateIndicator from '../LogStateIndicator/LogStateIndicator'; import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils'; @@ -27,8 +24,6 @@ import { UseTableViewResult, } from './types'; -const convert = new Convert(); - export const useTableView = (props: UseTableViewProps): UseTableViewResult => { const { logs, @@ -149,11 +144,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { children: ( () => void; } -const convert = new Convert(); - // Memoized Tree Component const MemoizedTree = React.memo<{ treeData: any[] }>(({ treeData }) => ( {}; } + // Avoid processing if the json is too large + const byteSize = new Blob([value]).size; + if (byteSize > MAX_BODY_BYTES) { + return (): void => {}; + } + processingRef.current = true; setJsonState({ isLoading: true, treeData: null, error: null }); diff --git a/frontend/src/container/LogDetailedView/util.test.ts b/frontend/src/container/LogDetailedView/util.test.ts index d5918f2bcae..f2131c253b3 100644 --- a/frontend/src/container/LogDetailedView/util.test.ts +++ b/frontend/src/container/LogDetailedView/util.test.ts @@ -1,6 +1,11 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { flattenObject, getDataTypes, recursiveParseJSON } from './utils'; +import { + flattenObject, + getDataTypes, + getSanitizedLogBody, + recursiveParseJSON, +} from './utils'; describe('recursiveParseJSON', () => { it('should return an empty object if the input is not valid JSON', () => { @@ -185,3 +190,146 @@ describe('Get Data Types utils', () => { expect(getDataTypes([2.5, 3, 1])).toBe(DataTypes.ArrayFloat64); }); }); + +describe('getSanitizedLogBody', () => { + it('should return sanitized HTML with default options (shouldEscapeHtml: false)', () => { + const input = 'Hello World'; + const result = getSanitizedLogBody(input); + + // Should remove script tags and return sanitized HTML + expect(result).not.toContain('Hello World'; + const result = getSanitizedLogBody(input, { shouldEscapeHtml: true }); + + // Should escape HTML entities + expect(result).toContain('<script>'); + expect(result).toContain('</script>'); + expect(result).toContain('Hello World'); + }); + + it('should handle ANSI color codes correctly', () => { + const input = '\x1b[32mHello\x1b[0m World'; + const result = getSanitizedLogBody(input); + + // Should convert ANSI codes to HTML spans + expect(result).toContain(' { + const input = 'Hello\\nWorld\\tTab'; + const result = getSanitizedLogBody(input); + + // Should unescape the string + expect(result).toContain('Hello'); + expect(result).toContain('World'); + }); + + it('should handle empty string input', () => { + const result = getSanitizedLogBody(''); + expect(result).toBe(''); + }); + + it('should handle null/undefined input gracefully', () => { + const result1 = getSanitizedLogBody(null as any); + const result2 = getSanitizedLogBody(undefined as any); + + expect(result1).toBe(''); + expect(result2).toBe(''); + }); + + it('should handle special characters and entities', () => { + const input = '& < > " \' & < >'; + const result = getSanitizedLogBody(input, { shouldEscapeHtml: true }); + + // Should escape HTML entities + expect(result).toContain('&'); + expect(result).toContain('<'); + expect(result).toContain('>'); + expect(result).toContain('"'); + }); + + it('should handle complex HTML with mixed content', () => { + const input = + '

Hello World

'; + const result = getSanitizedLogBody(input); + + // Should keep safe HTML but remove script tags + expect(result).toContain('
'); + expect(result).toContain('

'); + expect(result).toContain(''); + expect(result).toContain('Hello'); + expect(result).toContain('World'); + expect(result).not.toContain(''; + const result = getSanitizedLogBody(input, { shouldEscapeHtml: true }); + + // Should handle both ANSI codes and HTML escaping + expect(result).toContain(' => { try { const value = JSON.parse(obj); @@ -336,3 +341,21 @@ export function findKeyPath( }); return finalPath; } + +export const getSanitizedLogBody = ( + text: string, + options: { shouldEscapeHtml?: boolean } = {}, +): string => { + const { shouldEscapeHtml = false } = options; + const escapedText = shouldEscapeHtml ? escapeHtml(text) : text; + try { + return convertInstance.toHtml( + dompurify.sanitize(unescapeString(escapedText), { + FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], + }), + ); + } catch (error) { + console.error('Error sanitizing text', error, text); + return '{}'; + } +};