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.
| Package | Affected versions | Patched versions |
|---|---|---|
flowisenpm | < 3.0.5 | 3.0.5 |
Affected products
1Patches
19a06a85a8ddcChore/Safe Parse HTML (#4905)
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, '&').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' - } 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, '&').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>) } - 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- github.com/advisories/GHSA-7r4h-vmj9-wg42ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-29192ghsaADVISORY
- github.com/FlowiseAI/Flowise/commit/9a06a85a8ddcbaeca1342827a5fea9087a587d97ghsaWEB
- github.com/FlowiseAI/Flowise/pull/4905ghsaWEB
- github.com/FlowiseAI/Flowise/releases/tag/flowise%403.0.5ghsaWEB
- github.com/FlowiseAI/Flowise/security/advisories/GHSA-7r4h-vmj9-wg42ghsaWEB
News mentions
0No linked articles in our index yet.