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)
|
||||
.createQueryBuilder('execution')
|
||||
.leftJoinAndSelect('execution.agentflow', 'agentflow')
|
||||
.orderBy('execution.createdDate', 'DESC')
|
||||
.orderBy('execution.updatedDate', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "1.7.9",
|
||||
"clsx": "^1.1.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^16.0.0",
|
||||
"flowise-embed": "latest",
|
||||
"flowise-embed-react": "latest",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
|||
|
||||
// Project import
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
|
|
@ -860,7 +861,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||
} else if (item.type === 'html') {
|
||||
return (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: item.data }}></div>
|
||||
<SafeHTML html={item.data} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,31 +3,85 @@ import { Box } from '@mui/material'
|
|||
import { useTheme } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Syntax highlighting function for JSON
|
||||
function syntaxHighlight(json) {
|
||||
if (!json) return '' // No JSON from response
|
||||
const JsonToken = ({ type, children, isDarkMode }) => {
|
||||
const getTokenStyle = (tokenType) => {
|
||||
switch (tokenType) {
|
||||
case 'string':
|
||||
return { color: isDarkMode ? '#9cdcfe' : 'green' }
|
||||
case 'number':
|
||||
return { color: isDarkMode ? '#b5cea8' : 'darkorange' }
|
||||
case 'boolean':
|
||||
return { color: isDarkMode ? '#569cd6' : 'blue' }
|
||||
case 'null':
|
||||
return { color: isDarkMode ? '#d4d4d4' : 'magenta' }
|
||||
case 'key':
|
||||
return { color: isDarkMode ? '#ff5733' : '#ff5733' }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
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, '>')
|
||||
|
||||
return json.replace(
|
||||
// eslint-disable-next-line
|
||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
||||
function (match) {
|
||||
let cls = 'number'
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key'
|
||||
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 {
|
||||
cls = 'string'
|
||||
tokenType = 'string'
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean'
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null'
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>'
|
||||
} 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' }) => {
|
||||
|
|
@ -35,6 +89,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
|||
const customization = useSelector((state) => state.customization)
|
||||
const isDarkMode = customization.isDarkMode
|
||||
|
||||
const jsonString = JSON.stringify(data, null, 2)
|
||||
const jsonElements = parseJsonToElements(jsonString, isDarkMode)
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
|
@ -48,23 +105,6 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
|||
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
|
||||
style={{
|
||||
margin: 0,
|
||||
|
|
@ -73,10 +113,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
|||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: syntaxHighlight(JSON.stringify(data, null, 2), isDarkMode)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{jsonElements}
|
||||
</pre>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -85,3 +124,9 @@ JSONViewer.propTypes = {
|
|||
data: PropTypes.object,
|
||||
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
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||
import { AGENTFLOW_ICONS, baseURL } from '@/store/constant'
|
||||
import { JSONViewer } from '@/ui-component/json/JsonViewer'
|
||||
import ReactJson from 'flowise-react-json-view'
|
||||
|
|
@ -708,7 +709,7 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic,
|
|||
backgroundColor: theme.palette.background.paper
|
||||
}}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: artifact.data }}></div>
|
||||
<SafeHTML html={artifact.data} />
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import audioUploadSVG from '@/assets/images/wave-sound.jpg'
|
|||
// project import
|
||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { SafeHTML } from '@/ui-component/safe/SafeHTML'
|
||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
|
||||
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
||||
|
|
@ -1659,7 +1660,7 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP
|
|||
} else if (item.type === 'html') {
|
||||
return (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: item.data }}></div>
|
||||
<SafeHTML html={item.data} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -984,6 +984,9 @@ importers:
|
|||
clsx:
|
||||
specifier: ^1.1.1
|
||||
version: 1.2.1
|
||||
dompurify:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6
|
||||
dotenv:
|
||||
specifier: ^16.0.0
|
||||
version: 16.4.5
|
||||
|
|
@ -9941,6 +9944,9 @@ packages:
|
|||
resolution: { integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== }
|
||||
engines: { node: '>= 4' }
|
||||
|
||||
dompurify@3.2.6:
|
||||
resolution: { integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== }
|
||||
|
||||
domutils@1.7.0:
|
||||
resolution: { integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== }
|
||||
|
||||
|
|
@ -30197,6 +30203,10 @@ snapshots:
|
|||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.2.6:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
domutils@1.7.0:
|
||||
dependencies:
|
||||
dom-serializer: 0.2.2
|
||||
|
|
|
|||
Loading…
Reference in New Issue