VYPR
Critical severityNVD Advisory· Published Oct 6, 2025· Updated Oct 6, 2025

CVE-2025-50538

CVE-2025-50538

Description

Flowise before 3.0.5 allows XSS via an IFRAME element when an admin views the chat log.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
flowisenpm
< 3.0.83.0.8

Affected products

1

Patches

1
9a06a85a8ddc

Chore/Safe Parse HTML (#4905)

https://github.com/FlowiseAI/FlowiseHenry HengJul 20, 2025via ghsa
8 files changed · +165 48
  • packages/server/src/services/executions/index.ts+1 1 modified
    @@ -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)
     
    
  • packages/ui/package.json+1 0 modified
    @@ -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",
    
  • packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx+2 1 modified
    @@ -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 {
    
  • packages/ui/src/ui-component/json/JsonViewer.jsx+89 44 modified
    @@ -3,38 +3,95 @@ 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
    -
    -    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    -
    -    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'
    -                } else {
    -                    cls = 'string'
    -                }
    -            } else if (/true|false/.test(match)) {
    -                cls = 'boolean'
    -            } else if (/null/.test(match)) {
    -                cls = 'null'
    +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 {}
    +        }
    +    }
    +
    +    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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    +
    +    // 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>)
                 }
    -            return '<span class="' + cls + '">' + match + '</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' }) => {
         const theme = useTheme()
         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
    +}
    
  • packages/ui/src/ui-component/safe/SafeHTML.jsx+58 0 added
    @@ -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)
    +}
    
  • packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx+2 1 modified
    @@ -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 {
    
  • packages/ui/src/views/chatmessage/ChatMessage.jsx+2 1 modified
    @@ -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 {
    
  • pnpm-lock.yaml+10 0 modified
    @@ -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
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.