=== vulnerable Flowise 3.0.5 streamStorageFile === export const streamStorageFile = async ( chatflowId: string, chatId: string, fileName: string, orgId: string ): Promise => { // Validate chatflowId if (!chatflowId || !isValidUUID(chatflowId)) { throw new Error('Invalid chatflowId format - must be a valid UUID') } // Check for path traversal attempts if (isPathTraversal(chatflowId)) { throw new Error('Invalid path characters detected in chatflowId') } const storageType = getStorageType() const sanitizedFilename = sanitize(fileName) if (storageType === 's3') { const { s3Client, Bucket } = getS3Config() const Key = orgId + '/' + chatflowId + '/' + chatId + '/' + sanitizedFilename const getParams = { Bucket, Key } try { const response = await s3Client.send(new GetObjectCommand(getParams)) const body = response.Body if (body instanceof Readable) { const blob = await body.transformToByteArray() return Buffer.from(blob) } } catch (error) { // Fallback: Check if file exists without orgId const fallbackKey = chatflowId + '/' + chatId + '/' + sanitizedFilename try { const fallbackParams = { Bucket, Key: fallbackKey } const fallbackResponse = await s3Client.send(new GetObjectCommand(fallbackParams)) const fallbackBody = fallbackResponse.Body // If found, copy to correct location with orgId if (fallbackBody) { // Get the file content let fileContent: Buffer if (fallbackBody instanceof Readable) { const blob = await fallbackBody.transformToByteArray() fileContent = Buffer.from(blob) } else { // @ts-ignore fileContent = Buffer.concat(fallbackBody.toArray()) } // Move to correct location with orgId const putObjCmd = new PutObjectCommand({ Bucket, Key, Body: fileContent }) await s3Client.send(putObjCmd) // Delete the old file await s3Client.send( new DeleteObjectsCommand({ Bucket, Delete: { Objects: [{ Key: fallbackKey }], Quiet: false } }) ) // Check if the directory is empty and delete recursively if needed await _cleanEmptyS3Folders(s3Client, Bucket, chatflowId) return fileContent } } catch (fallbackError) { // File not found in fallback location either throw new Error(`File ${fileName} not found`) } } } else if (storageType === 'gcs') { const { bucket } = getGcsClient() const normalizedChatflowId = chatflowId.replace(/\\/g, '/') const normalizedChatId = chatId.replace(/\\/g, '/') const normalizedFilename = sanitizedFilename.replace(/\\/g, '/') const filePath = `${orgId}/${normalizedChatflowId}/${normalizedChatId}/${normalizedFilename}` try { const [buffer] = await bucket.file(filePath).download() return buffer } catch (error) { // Fallback: Check if file exists without orgId const fallbackPath = `${normalizedChatflowId}/${normalizedChatId}/${normalizedFilename}` try { const fallbackFile = bucket.file(fallbackPath) const [buffer] = await fallbackFile.download() // If found, copy to correct location with orgId if (buffer) { const file = bucket.file(filePath) await new Promise((resolve, reject) => { file.createWriteStream() === fixed Flowise 3.0.6 security diff === diff --git a/packages/components/src/storageUtils.ts b/packages/components/src/storageUtils.ts index 954609a2..ff48bb05 100644 --- a/packages/components/src/storageUtils.ts +++ b/packages/components/src/storageUtils.ts @@ -753,8 +753,8 @@ export const streamStorageFile = async ( } // Check for path traversal attempts - if (isPathTraversal(chatflowId)) { - throw new Error('Invalid path characters detected in chatflowId') + if (isPathTraversal(chatflowId) || isPathTraversal(chatId)) { + throw new Error('Invalid path characters detected in chatflowId or chatId') } const storageType = getStorageType() @@ -1036,15 +1036,12 @@ export const getGcsClient = () => { const projectId = process.env.GOOGLE_CLOUD_STORAGE_PROJ_ID const bucketName = process.env.GOOGLE_CLOUD_STORAGE_BUCKET_NAME - if (!pathToGcsCredential) { - throw new Error('GOOGLE_CLOUD_STORAGE_CREDENTIAL env variable is required') - } if (!bucketName) { throw new Error('GOOGLE_CLOUD_STORAGE_BUCKET_NAME env variable is required') } const storageConfig = { - keyFilename: pathToGcsCredential, + ...(pathToGcsCredential ? { keyFilename: pathToGcsCredential } : {}), ...(projectId ? { projectId } : {}) } === endpoint whitelist and controller === import Auth0SSO from '../enterprise/sso/Auth0SSO' import AzureSSO from '../enterprise/sso/AzureSSO' import GithubSSO from '../enterprise/sso/GithubSSO' import GoogleSSO from '../enterprise/sso/GoogleSSO' export const WHITELIST_URLS = [ '/api/v1/verify/apikey/', '/api/v1/chatflows/apikey/', '/api/v1/public-chatflows', '/api/v1/public-chatbotConfig', '/api/v1/public-executions', '/api/v1/prediction/', '/api/v1/vector/upsert/', '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', '/api/v1/chatflows-uploads', '/api/v1/openai-assistants-file/download', '/api/v1/feedback', '/api/v1/leads', '/api/v1/get-upload-file', '/api/v1/ip', '/api/v1/ping', '/api/v1/version', '/api/v1/attachments', '/api/v1/metrics', '/api/v1/nvidia-nim', '/api/v1/auth/resolve', import { Request, Response, NextFunction } from 'express' import fs from 'fs' import contentDisposition from 'content-disposition' import { streamStorageFile } from 'flowise-components' import { StatusCodes } from 'http-status-codes' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { ChatFlow } from '../../database/entities/ChatFlow' import { Workspace } from '../../enterprise/database/entities/workspace.entity' const streamUploadedFile = async (req: Request, res: Response, next: NextFunction) => { try { if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) { return res.status(500).send(`Invalid file path`) } const chatflowId = req.query.chatflowId as string const chatId = req.query.chatId as string const fileName = req.query.fileName as string const download = req.query.download === 'true' // Check if download parameter is set const appServer = getRunningExpressApp() // This can be public API, so we can only get orgId from the chatflow const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId }) if (!chatflow) { throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowId} not found`) } const chatflowWorkspaceId = chatflow.workspaceId const workspace = await appServer.AppDataSource.getRepository(Workspace).findOneBy({ id: chatflowWorkspaceId }) if (!workspace) { throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Workspace ${chatflowWorkspaceId} not found`) } const orgId = workspace.organizationId as string // Set Content-Disposition header - force attachment for download if (download) { res.setHeader('Content-Disposition', contentDisposition(fileName, { type: 'attachment' })) } else { res.setHeader('Content-Disposition', contentDisposition(fileName)) } const fileStream = await streamStorageFile(chatflowId, chatId, fileName, orgId) if (!fileStream) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: streamStorageFile`) if (fileStream instanceof fs.ReadStream && fileStream?.pipe) { fileStream.pipe(res) } else { res.send(fileStream) } } catch (error) { next(error) } } export default { streamUploadedFile }