Chore/Safe Parse HTML (#4905)
Refactor: Update pnpm-lock.yaml and enhance UI components for safe HTML rendering - Updated pnpm-lock.yaml to improve dependency management and ensure consistency. - Refactored the JSONViewer component to utilize a new JsonToken for syntax highlighting. - Introduced SafeHTML component to sanitize and safely render HTML content in ViewMessagesDialog and NodeExecutionDetails. - Replaced direct HTML rendering with SafeHTML in ChatMessage component for enhanced security.
This commit is contained in:
parent
96a57a58e7
commit
9a06a85a8d
|
|
@ -72,7 +72,7 @@ const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data:
|
||||||
const queryBuilder = appServer.AppDataSource.getRepository(Execution)
|
const queryBuilder = appServer.AppDataSource.getRepository(Execution)
|
||||||
.createQueryBuilder('execution')
|
.createQueryBuilder('execution')
|
||||||
.leftJoinAndSelect('execution.agentflow', 'agentflow')
|
.leftJoinAndSelect('execution.agentflow', 'agentflow')
|
||||||
.orderBy('execution.createdDate', 'DESC')
|
.orderBy('execution.updatedDate', 'DESC')
|
||||||
.skip((page - 1) * limit)
|
.skip((page - 1) * limit)
|
||||||
.take(limit)
|
.take(limit)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"@uiw/react-codemirror": "^4.21.21",
|
"@uiw/react-codemirror": "^4.21.21",
|
||||||
"axios": "1.7.9",
|
"axios": "1.7.9",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"flowise-embed": "latest",
|
"flowise-embed": "latest",
|
||||||
"flowise-embed-react": "latest",
|
"flowise-embed-react": "latest",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||||
|
|
||||||
// Project import
|
// Project import
|
||||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||||
|
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||||
|
|
@ -860,7 +861,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
} else if (item.type === 'html') {
|
} else if (item.type === 'html') {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '20px' }}>
|
<div style={{ marginTop: '20px' }}>
|
||||||
<div dangerouslySetInnerHTML={{ __html: item.data }}></div>
|
<SafeHTML html={item.data} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,31 +3,85 @@ import { Box } from '@mui/material'
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
// Syntax highlighting function for JSON
|
const JsonToken = ({ type, children, isDarkMode }) => {
|
||||||
function syntaxHighlight(json) {
|
const getTokenStyle = (tokenType) => {
|
||||||
if (!json) return '' // No JSON from response
|
switch (tokenType) {
|
||||||
|
case 'string':
|
||||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
return { color: isDarkMode ? '#9cdcfe' : 'green' }
|
||||||
|
case 'number':
|
||||||
return json.replace(
|
return { color: isDarkMode ? '#b5cea8' : 'darkorange' }
|
||||||
// eslint-disable-next-line
|
case 'boolean':
|
||||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
return { color: isDarkMode ? '#569cd6' : 'blue' }
|
||||||
function (match) {
|
case 'null':
|
||||||
let cls = 'number'
|
return { color: isDarkMode ? '#d4d4d4' : 'magenta' }
|
||||||
if (/^"/.test(match)) {
|
case 'key':
|
||||||
if (/:$/.test(match)) {
|
return { color: isDarkMode ? '#ff5733' : '#ff5733' }
|
||||||
cls = 'key'
|
default:
|
||||||
} else {
|
return {}
|
||||||
cls = 'string'
|
|
||||||
}
|
|
||||||
} else if (/true|false/.test(match)) {
|
|
||||||
cls = 'boolean'
|
|
||||||
} else if (/null/.test(match)) {
|
|
||||||
cls = 'null'
|
|
||||||
}
|
|
||||||
return '<span class="' + cls + '">' + match + '</span>'
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return <span style={getTokenStyle(type)}>{children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonToElements(json, isDarkMode) {
|
||||||
|
if (!json) return []
|
||||||
|
|
||||||
|
const tokens = []
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
// Escape HTML characters for safety
|
||||||
|
const escapedJson = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const tokenRegex = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g
|
||||||
|
|
||||||
|
let match
|
||||||
|
let lastIndex = 0
|
||||||
|
|
||||||
|
while ((match = tokenRegex.exec(escapedJson)) !== null) {
|
||||||
|
// Add any text before the match as plain text
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
const plainText = escapedJson.substring(lastIndex, match.index)
|
||||||
|
if (plainText) {
|
||||||
|
tokens.push(<span key={`plain-${index++}`}>{plainText}</span>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine token type
|
||||||
|
let tokenType = 'number'
|
||||||
|
const matchText = match[0]
|
||||||
|
|
||||||
|
if (/^"/.test(matchText)) {
|
||||||
|
if (/:$/.test(matchText)) {
|
||||||
|
tokenType = 'key'
|
||||||
|
} else {
|
||||||
|
tokenType = 'string'
|
||||||
|
}
|
||||||
|
} else if (/true|false/.test(matchText)) {
|
||||||
|
tokenType = 'boolean'
|
||||||
|
} else if (/null/.test(matchText)) {
|
||||||
|
tokenType = 'null'
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(
|
||||||
|
<JsonToken key={`token-${index++}`} type={tokenType} isDarkMode={isDarkMode}>
|
||||||
|
{matchText}
|
||||||
|
</JsonToken>
|
||||||
|
)
|
||||||
|
|
||||||
|
lastIndex = match.index + match[0].length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text
|
||||||
|
if (lastIndex < escapedJson.length) {
|
||||||
|
const remainingText = escapedJson.substring(lastIndex)
|
||||||
|
if (remainingText) {
|
||||||
|
tokens.push(<span key={`remaining-${index++}`}>{remainingText}</span>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
||||||
|
|
@ -35,6 +89,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
||||||
const customization = useSelector((state) => state.customization)
|
const customization = useSelector((state) => state.customization)
|
||||||
const isDarkMode = customization.isDarkMode
|
const isDarkMode = customization.isDarkMode
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(data, null, 2)
|
||||||
|
const jsonElements = parseJsonToElements(jsonString, isDarkMode)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -48,23 +105,6 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
||||||
maxHeight: maxHeight
|
maxHeight: maxHeight
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
|
||||||
pre .string {
|
|
||||||
color: ${isDarkMode ? '#9cdcfe' : 'green'};
|
|
||||||
}
|
|
||||||
pre .number {
|
|
||||||
color: ${isDarkMode ? '#b5cea8' : 'darkorange'};
|
|
||||||
}
|
|
||||||
pre .boolean {
|
|
||||||
color: ${isDarkMode ? '#569cd6' : 'blue'};
|
|
||||||
}
|
|
||||||
pre .null {
|
|
||||||
color: ${isDarkMode ? '#d4d4d4' : 'magenta'};
|
|
||||||
}
|
|
||||||
pre .key {
|
|
||||||
color: ${isDarkMode ? '#ff5733' : '#ff5733'};
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<pre
|
<pre
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
|
@ -73,10 +113,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word'
|
wordBreak: 'break-word'
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
>
|
||||||
__html: syntaxHighlight(JSON.stringify(data, null, 2), isDarkMode)
|
{jsonElements}
|
||||||
}}
|
</pre>
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -85,3 +124,9 @@ JSONViewer.propTypes = {
|
||||||
data: PropTypes.object,
|
data: PropTypes.object,
|
||||||
maxHeight: PropTypes.string
|
maxHeight: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonToken.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
isDarkMode: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SafeHTML component that sanitizes HTML content before rendering
|
||||||
|
*/
|
||||||
|
export const SafeHTML = ({ html, allowedTags, allowedAttributes, ...props }) => {
|
||||||
|
// Configure DOMPurify options
|
||||||
|
const config = {
|
||||||
|
ALLOWED_TAGS: allowedTags || [
|
||||||
|
'p',
|
||||||
|
'br',
|
||||||
|
'strong',
|
||||||
|
'em',
|
||||||
|
'u',
|
||||||
|
'i',
|
||||||
|
'b',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'ul',
|
||||||
|
'ol',
|
||||||
|
'li',
|
||||||
|
'blockquote',
|
||||||
|
'pre',
|
||||||
|
'code',
|
||||||
|
'a',
|
||||||
|
'img',
|
||||||
|
'table',
|
||||||
|
'thead',
|
||||||
|
'tbody',
|
||||||
|
'tr',
|
||||||
|
'th',
|
||||||
|
'td',
|
||||||
|
'div',
|
||||||
|
'span'
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: allowedAttributes || ['href', 'title', 'alt', 'src', 'class', 'id', 'style'],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
FORBID_SCRIPT: true,
|
||||||
|
FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input'],
|
||||||
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize the HTML content
|
||||||
|
const sanitizedHTML = DOMPurify.sanitize(html || '', config)
|
||||||
|
|
||||||
|
return <div {...props} dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
SafeHTML.propTypes = {
|
||||||
|
html: PropTypes.string.isRequired,
|
||||||
|
allowedTags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
allowedAttributes: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ import toolSVG from '@/assets/images/tool.svg'
|
||||||
|
|
||||||
// Project imports
|
// Project imports
|
||||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||||
|
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||||
import { AGENTFLOW_ICONS, baseURL } from '@/store/constant'
|
import { AGENTFLOW_ICONS, baseURL } from '@/store/constant'
|
||||||
import { JSONViewer } from '@/ui-component/json/JsonViewer'
|
import { JSONViewer } from '@/ui-component/json/JsonViewer'
|
||||||
import ReactJson from 'flowise-react-json-view'
|
import ReactJson from 'flowise-react-json-view'
|
||||||
|
|
@ -708,7 +709,7 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic,
|
||||||
backgroundColor: theme.palette.background.paper
|
backgroundColor: theme.palette.background.paper
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div dangerouslySetInnerHTML={{ __html: artifact.data }}></div>
|
<SafeHTML html={artifact.data} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import audioUploadSVG from '@/assets/images/wave-sound.jpg'
|
||||||
// project import
|
// project import
|
||||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||||
|
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||||
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
|
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
|
||||||
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
||||||
|
|
@ -1659,7 +1660,7 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP
|
||||||
} else if (item.type === 'html') {
|
} else if (item.type === 'html') {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '20px' }}>
|
<div style={{ marginTop: '20px' }}>
|
||||||
<div dangerouslySetInnerHTML={{ __html: item.data }}></div>
|
<SafeHTML html={item.data} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -984,6 +984,9 @@ importers:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
|
dompurify:
|
||||||
|
specifier: ^3.2.6
|
||||||
|
version: 3.2.6
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
|
|
@ -9941,6 +9944,9 @@ packages:
|
||||||
resolution: { integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== }
|
resolution: { integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== }
|
||||||
engines: { node: '>= 4' }
|
engines: { node: '>= 4' }
|
||||||
|
|
||||||
|
dompurify@3.2.6:
|
||||||
|
resolution: { integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== }
|
||||||
|
|
||||||
domutils@1.7.0:
|
domutils@1.7.0:
|
||||||
resolution: { integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== }
|
resolution: { integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== }
|
||||||
|
|
||||||
|
|
@ -30197,6 +30203,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
dompurify@3.2.6:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
domutils@1.7.0:
|
domutils@1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dom-serializer: 0.2.2
|
dom-serializer: 0.2.2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue