VYPR
Critical severityNVD Advisory· Published Oct 8, 2025· Updated Oct 14, 2025

Flowise is vulnerable to arbitrary file read, arbitrary file write

CVE-2025-61913

Description

Flowise is a drag & drop user interface to build a customized large language model flow. In versions prior to 3.0.8, WriteFileTool and ReadFileTool in Flowise do not restrict file path access, allowing authenticated attackers to exploit this vulnerability to read and write arbitrary files to any path in the file system, potentially leading to remote command execution. Flowise 3.0.8 fixes this vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
flowisenpm
< 3.0.83.0.8
flowise-componentsnpm
< 3.0.83.0.8
Flowisenpm
< 3.0.83.0.8

Affected products

1

Patches

1
1fb12cd93143

Chore/read write tools update (#5275)

https://github.com/FlowiseAI/FlowiseHenry HengOct 8, 2025via ghsa
8 files changed · +392 22
  • packages/components/nodes/tools/ReadFile/ReadFile.ts+70 8 modified
    @@ -1,9 +1,10 @@
     import { z } from 'zod'
    +import path from 'path'
     import { StructuredTool, ToolParams } from '@langchain/core/tools'
     import { Serializable } from '@langchain/core/load/serializable'
    -import { NodeFileStore } from 'langchain/stores/file/node'
     import { INode, INodeData, INodeParams } from '../../../src/Interface'
    -import { getBaseClasses } from '../../../src/utils'
    +import { getBaseClasses, getUserHome } from '../../../src/utils'
    +import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
     
     abstract class BaseFileStore extends Serializable {
         abstract readFile(path: string): Promise<string>
    @@ -20,30 +21,91 @@ class ReadFile_Tools implements INode {
         category: string
         baseClasses: string[]
         inputs: INodeParams[]
    +    warning: string
     
         constructor() {
             this.label = 'Read File'
             this.name = 'readFile'
    -        this.version = 1.0
    +        this.version = 2.0
             this.type = 'ReadFile'
             this.icon = 'readfile.svg'
             this.category = 'Tools'
    +        this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.'
             this.description = 'Read file from disk'
             this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)]
             this.inputs = [
                 {
    -                label: 'Base Path',
    -                name: 'basePath',
    -                placeholder: `C:\\Users\\User\\Desktop`,
    +                label: 'Workspace Path',
    +                name: 'workspacePath',
    +                placeholder: `C:\\Users\\User\\MyProject`,
                     type: 'string',
    +                description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
    +                optional: true
    +            },
    +            {
    +                label: 'Enforce Workspace Boundaries',
    +                name: 'enforceWorkspaceBoundaries',
    +                type: 'boolean',
    +                description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
    +                default: true,
    +                optional: true
    +            },
    +            {
    +                label: 'Max File Size (MB)',
    +                name: 'maxFileSize',
    +                type: 'number',
    +                description: 'Maximum file size in megabytes that can be read',
    +                default: 10,
    +                optional: true
    +            },
    +            {
    +                label: 'Allowed Extensions',
    +                name: 'allowedExtensions',
    +                type: 'string',
    +                description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
    +                placeholder: '.txt,.json,.md,.py,.js',
                     optional: true
                 }
             ]
         }
     
         async init(nodeData: INodeData): Promise<any> {
    -        const basePath = nodeData.inputs?.basePath as string
    -        const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
    +        const workspacePath = nodeData.inputs?.workspacePath as string
    +        const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
    +        const maxFileSize = nodeData.inputs?.maxFileSize as number
    +        const allowedExtensions = nodeData.inputs?.allowedExtensions as string
    +
    +        // Parse allowed extensions
    +        const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
    +
    +        let store: BaseFileStore
    +
    +        if (workspacePath) {
    +            // Create secure file store with workspace boundaries
    +            const config: FileSecurityConfig = {
    +                workspacePath,
    +                enforceWorkspaceBoundaries,
    +                maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
    +                allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
    +            }
    +            store = new SecureFileStore(config)
    +        } else {
    +            // Fallback to current working directory with security warnings
    +            if (enforceWorkspaceBoundaries) {
    +                const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
    +                console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
    +                store = new SecureFileStore({
    +                    workspacePath: fallbackWorkspacePath,
    +                    enforceWorkspaceBoundaries: true,
    +                    maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
    +                    allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
    +                })
    +            } else {
    +                console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
    +                store = SecureFileStore.createUnsecure()
    +            }
    +        }
    +
             return new ReadFileTool({ store })
         }
     }
    
  • packages/components/nodes/tools/WriteFile/WriteFile.ts+72 10 modified
    @@ -1,9 +1,10 @@
     import { z } from 'zod'
    +import path from 'path'
     import { StructuredTool, ToolParams } from '@langchain/core/tools'
     import { Serializable } from '@langchain/core/load/serializable'
    -import { NodeFileStore } from 'langchain/stores/file/node'
     import { INode, INodeData, INodeParams } from '../../../src/Interface'
    -import { getBaseClasses } from '../../../src/utils'
    +import { getBaseClasses, getUserHome } from '../../../src/utils'
    +import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
     
     abstract class BaseFileStore extends Serializable {
         abstract readFile(path: string): Promise<string>
    @@ -20,30 +21,91 @@ class WriteFile_Tools implements INode {
         category: string
         baseClasses: string[]
         inputs: INodeParams[]
    +    warning: string
     
         constructor() {
             this.label = 'Write File'
             this.name = 'writeFile'
    -        this.version = 1.0
    +        this.version = 2.0
             this.type = 'WriteFile'
             this.icon = 'writefile.svg'
             this.category = 'Tools'
    +        this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.'
             this.description = 'Write file to disk'
             this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)]
             this.inputs = [
                 {
    -                label: 'Base Path',
    -                name: 'basePath',
    -                placeholder: `C:\\Users\\User\\Desktop`,
    +                label: 'Workspace Path',
    +                name: 'workspacePath',
    +                placeholder: `C:\\Users\\User\\MyProject`,
                     type: 'string',
    +                description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
    +                optional: true
    +            },
    +            {
    +                label: 'Enforce Workspace Boundaries',
    +                name: 'enforceWorkspaceBoundaries',
    +                type: 'boolean',
    +                description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
    +                default: true,
    +                optional: true
    +            },
    +            {
    +                label: 'Max File Size (MB)',
    +                name: 'maxFileSize',
    +                type: 'number',
    +                description: 'Maximum file size in megabytes that can be written',
    +                default: 10,
    +                optional: true
    +            },
    +            {
    +                label: 'Allowed Extensions',
    +                name: 'allowedExtensions',
    +                type: 'string',
    +                description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
    +                placeholder: '.txt,.json,.md,.py,.js',
                     optional: true
                 }
             ]
         }
     
         async init(nodeData: INodeData): Promise<any> {
    -        const basePath = nodeData.inputs?.basePath as string
    -        const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
    +        const workspacePath = nodeData.inputs?.workspacePath as string
    +        const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
    +        const maxFileSize = nodeData.inputs?.maxFileSize as number
    +        const allowedExtensions = nodeData.inputs?.allowedExtensions as string
    +
    +        // Parse allowed extensions
    +        const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
    +
    +        let store: BaseFileStore
    +
    +        if (workspacePath) {
    +            // Create secure file store with workspace boundaries
    +            const config: FileSecurityConfig = {
    +                workspacePath,
    +                enforceWorkspaceBoundaries,
    +                maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
    +                allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
    +            }
    +            store = new SecureFileStore(config)
    +        } else {
    +            // Fallback to current working directory with security warnings
    +            if (enforceWorkspaceBoundaries) {
    +                const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
    +                console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
    +                store = new SecureFileStore({
    +                    workspacePath: fallbackWorkspacePath,
    +                    enforceWorkspaceBoundaries: true,
    +                    maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
    +                    allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
    +                })
    +            } else {
    +                console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
    +                store = SecureFileStore.createUnsecure()
    +            }
    +        }
    +
             return new WriteFileTool({ store })
         }
     }
    @@ -68,7 +130,7 @@ export class WriteFileTool extends StructuredTool {
     
         name = 'write_file'
     
    -    description = 'Write file from disk'
    +    description = 'Write file to disk'
     
         store: BaseFileStore
     
    @@ -80,7 +142,7 @@ export class WriteFileTool extends StructuredTool {
     
         async _call({ file_path, text }: z.infer<typeof this.schema>) {
             await this.store.writeFile(file_path, text)
    -        return 'File written to successfully.'
    +        return `File written to ${file_path} successfully.`
         }
     }
     
    
  • packages/components/src/Interface.ts+1 0 modified
    @@ -134,6 +134,7 @@ export interface INodeProperties {
         documentation?: string
         color?: string
         hint?: string
    +    warning?: string
     }
     
     export interface INode extends INodeProperties {
    
  • packages/components/src/SecureFileStore.ts+167 0 added
    @@ -0,0 +1,167 @@
    +import { Serializable } from '@langchain/core/load/serializable'
    +import { NodeFileStore } from 'langchain/stores/file/node'
    +import { isUnsafeFilePath, isWithinWorkspace } from './validator'
    +import * as path from 'path'
    +import * as fs from 'fs'
    +
    +/**
    + * Security configuration for file operations
    + */
    +export interface FileSecurityConfig {
    +    /** Base workspace path - all file operations are restricted to this directory */
    +    workspacePath: string
    +    /** Whether to enforce workspace boundaries (default: true) */
    +    enforceWorkspaceBoundaries?: boolean
    +    /** Maximum file size in bytes (default: 10MB) */
    +    maxFileSize?: number
    +    /** Allowed file extensions (if empty, all extensions allowed) */
    +    allowedExtensions?: string[]
    +    /** Blocked file extensions */
    +    blockedExtensions?: string[]
    +}
    +
    +/**
    + * Secure file store that enforces workspace boundaries and validates file operations
    + */
    +export class SecureFileStore extends Serializable {
    +    lc_namespace = ['flowise', 'components', 'stores', 'file']
    +
    +    private config: Required<FileSecurityConfig>
    +    private nodeFileStore: NodeFileStore
    +
    +    constructor(config: FileSecurityConfig) {
    +        super()
    +
    +        // Set default configuration
    +        this.config = {
    +            workspacePath: config.workspacePath,
    +            enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true,
    +            maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default
    +            allowedExtensions: config.allowedExtensions ?? [],
    +            blockedExtensions: config.blockedExtensions ?? [
    +                '.exe',
    +                '.bat',
    +                '.cmd',
    +                '.sh',
    +                '.ps1',
    +                '.vbs',
    +                '.scr',
    +                '.com',
    +                '.pif',
    +                '.dll',
    +                '.sys',
    +                '.msi',
    +                '.jar'
    +            ]
    +        }
    +
    +        // Validate workspace path
    +        if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) {
    +            throw new Error('Workspace path must be an absolute path')
    +        }
    +
    +        // Ensure workspace directory exists
    +        if (!fs.existsSync(this.config.workspacePath)) {
    +            throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`)
    +        }
    +
    +        // Initialize the underlying NodeFileStore with workspace path
    +        this.nodeFileStore = new NodeFileStore(this.config.workspacePath)
    +    }
    +
    +    /**
    +     * Validates a file path against security policies
    +     */
    +    private validateFilePath(filePath: string): void {
    +        // Check for unsafe path patterns
    +        if (isUnsafeFilePath(filePath)) {
    +            throw new Error(`Unsafe file path detected: ${filePath}`)
    +        }
    +
    +        // Enforce workspace boundaries if enabled
    +        if (this.config.enforceWorkspaceBoundaries) {
    +            if (!isWithinWorkspace(filePath, this.config.workspacePath)) {
    +                throw new Error(`File path outside workspace boundaries: ${filePath}`)
    +            }
    +        }
    +
    +        // Check file extension
    +        const ext = path.extname(filePath).toLowerCase()
    +
    +        // Check blocked extensions
    +        if (this.config.blockedExtensions.includes(ext)) {
    +            throw new Error(`File extension not allowed: ${ext}`)
    +        }
    +
    +        // Check allowed extensions (if specified)
    +        if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) {
    +            throw new Error(`File extension not in allowed list: ${ext}`)
    +        }
    +    }
    +
    +    /**
    +     * Validates file size
    +     */
    +    private validateFileSize(content: string): void {
    +        const sizeInBytes = Buffer.byteLength(content, 'utf8')
    +        if (sizeInBytes > this.config.maxFileSize) {
    +            throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`)
    +        }
    +    }
    +
    +    /**
    +     * Reads a file with security validation
    +     */
    +    async readFile(filePath: string): Promise<string> {
    +        this.validateFilePath(filePath)
    +
    +        try {
    +            return await this.nodeFileStore.readFile(filePath)
    +        } catch (error) {
    +            // Provide generic error message to avoid information leakage
    +            throw new Error(`Failed to read file: ${path.basename(filePath)}`)
    +        }
    +    }
    +
    +    /**
    +     * Writes a file with security validation
    +     */
    +    async writeFile(filePath: string, contents: string): Promise<void> {
    +        this.validateFilePath(filePath)
    +        this.validateFileSize(contents)
    +
    +        try {
    +            // Ensure the directory exists
    +            const dir = path.dirname(path.resolve(this.config.workspacePath, filePath))
    +            if (!fs.existsSync(dir)) {
    +                fs.mkdirSync(dir, { recursive: true })
    +            }
    +
    +            await this.nodeFileStore.writeFile(filePath, contents)
    +        } catch (error) {
    +            // Provide generic error message to avoid information leakage
    +            throw new Error(`Failed to write file: ${path.basename(filePath)}`)
    +        }
    +    }
    +
    +    /**
    +     * Gets the workspace configuration
    +     */
    +    getConfig(): Readonly<Required<FileSecurityConfig>> {
    +        return { ...this.config }
    +    }
    +
    +    /**
    +     * Creates a secure file store with workspace enforcement disabled (for backward compatibility)
    +     * WARNING: This should only be used when absolutely necessary and with proper user consent
    +     */
    +    static createUnsecure(basePath?: string): SecureFileStore {
    +        const workspacePath = basePath || process.cwd()
    +        return new SecureFileStore({
    +            workspacePath,
    +            enforceWorkspaceBoundaries: false,
    +            maxFileSize: 50 * 1024 * 1024, // 50MB for unsecure mode
    +            blockedExtensions: [] // No extension restrictions in unsecure mode
    +        })
    +    }
    +}
    
  • packages/components/src/validator.ts+61 0 modified
    @@ -41,3 +41,64 @@ export const isPathTraversal = (path: string): boolean => {
     
         return dangerousPatterns.some((pattern) => path.toLowerCase().includes(pattern))
     }
    +
    +/**
    + * Enhanced path validation for workspace-scoped file operations
    + * @param {string} filePath The file path to validate
    + * @returns {boolean} True if path traversal detected, false otherwise
    + */
    +export const isUnsafeFilePath = (filePath: string): boolean => {
    +    if (!filePath || typeof filePath !== 'string') {
    +        return true
    +    }
    +
    +    // Check for path traversal patterns
    +    const dangerousPatterns = [
    +        /\.\./, // Directory traversal (..)
    +        /%2e%2e/i, // URL encoded ..
    +        /%2f/i, // URL encoded /
    +        /%5c/i, // URL encoded \
    +        /\0/, // Null bytes
    +        // eslint-disable-next-line no-control-regex
    +        /[\x00-\x1f]/, // Control characters
    +        /^\/[^/]/, // Absolute Unix paths (starting with /)
    +        /^[a-zA-Z]:\\/, // Absolute Windows paths (C:\)
    +        /^\\\\[^\\]/, // UNC paths (\\server\)
    +        /^\\\\\?\\/ // Extended-length paths (\\?\)
    +    ]
    +
    +    return dangerousPatterns.some((pattern) => pattern.test(filePath))
    +}
    +
    +/**
    + * Validates if a file path is within the allowed workspace boundaries
    + * @param {string} filePath The file path to validate
    + * @param {string} workspacePath The workspace base path
    + * @returns {boolean} True if path is within workspace, false otherwise
    + */
    +export const isWithinWorkspace = (filePath: string, workspacePath: string): boolean => {
    +    if (!filePath || !workspacePath) {
    +        return false
    +    }
    +
    +    try {
    +        const path = require('path')
    +
    +        // Resolve both paths to absolute paths
    +        const resolvedFilePath = path.resolve(workspacePath, filePath)
    +        const resolvedWorkspacePath = path.resolve(workspacePath)
    +
    +        // Normalize paths to handle different separators
    +        const normalizedFilePath = path.normalize(resolvedFilePath)
    +        const normalizedWorkspacePath = path.normalize(resolvedWorkspacePath)
    +
    +        // Check if the file path starts with the workspace path
    +        const relativePath = path.relative(normalizedWorkspacePath, normalizedFilePath)
    +
    +        // If relative path starts with '..' or is absolute, it's outside workspace
    +        return !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
    +    } catch (error) {
    +        // If any error occurs during path resolution, deny access
    +        return false
    +    }
    +}
    
  • packages/ui/src/views/agentflowsv2/AgentFlowNode.jsx+2 0 modified
    @@ -191,6 +191,8 @@ const AgentFlowNode = ({ data }) => {
                         componentNode?.deprecateMessage ??
                             'This node will be deprecated in the next release. Change to a new node tagged with NEW'
                     )
    +            } else if (componentNode.warning) {
    +                setWarningMessage(componentNode.warning)
                 } else {
                     setWarningMessage('')
                 }
    
  • packages/ui/src/views/agentflowsv2/ConfigInput.jsx+17 4 modified
    @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'
     import { cloneDeep } from 'lodash'
     
     // Material
    -import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from '@mui/material'
    +import { Accordion, AccordionSummary, AccordionDetails, Box, Typography, Tooltip, IconButton } from '@mui/material'
     import { useTheme } from '@mui/material/styles'
     import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
    -import { IconSettings } from '@tabler/icons-react'
    +import { IconSettings, IconAlertTriangle } from '@tabler/icons-react'
     
     // Project imports
     import NodeInputHandler from '../canvas/NodeInputHandler'
    @@ -292,8 +292,21 @@ export const ConfigInput = ({ data, inputParam, disabled = false, arrayIndex = n
                 >
                     <Accordion sx={{ background: 'transparent' }} expanded={expanded} onChange={handleAccordionChange}>
                         <AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ background: 'transparent' }}>
    -                        <IconSettings stroke={1.5} size='1.3rem' />
    -                        <Typography sx={{ ml: 1 }}>{selectedComponentNodeData?.label} Parameters</Typography>
    +                        <div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
    +                            <IconSettings stroke={1.5} size='1.3rem' />
    +                            <Typography sx={{ ml: 1 }}>{selectedComponentNodeData?.label} Parameters</Typography>
    +                            <div style={{ flexGrow: 1 }}></div>
    +                            {selectedComponentNodeData?.warning && (
    +                                <Tooltip
    +                                    title={<span style={{ whiteSpace: 'pre-line' }}>{selectedComponentNodeData.warning}</span>}
    +                                    placement='top'
    +                                >
    +                                    <IconButton sx={{ height: 35, width: 35 }}>
    +                                        <IconAlertTriangle size={20} color='orange' />
    +                                    </IconButton>
    +                                </Tooltip>
    +                            )}
    +                        </div>
                         </AccordionSummary>
                         <AccordionDetails>
                             {(selectedComponentNodeData.inputParams ?? [])
    
  • packages/ui/src/views/canvas/CanvasNode.jsx+2 0 modified
    @@ -82,6 +82,8 @@ const CanvasNode = ({ data }) => {
                         componentNode?.deprecateMessage ??
                             'This node will be deprecated in the next release. Change to a new node tagged with NEW'
                     )
    +            } else if (componentNode.warning) {
    +                setWarningMessage(componentNode.warning)
                 } else {
                     setWarningMessage('')
                 }
    

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

7

News mentions

0

No linked articles in our index yet.