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

CVE-2025-29192

CVE-2025-29192

Description

Flowise before 3.0.5 allows XSS via a FORM element and an INPUT element when an admin views the chat log.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
flowisenpm
< 3.0.53.0.5

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.