diff --git a/.github/workflows/docker-image-ecr.yml b/.github/workflows/docker-image-ecr.yml index ee78efb01..53efe43aa 100644 --- a/.github/workflows/docker-image-ecr.yml +++ b/.github/workflows/docker-image-ecr.yml @@ -25,7 +25,7 @@ on: default: 'latest' permissions: - contents: read # Required for checkout + contents: read # Required for checkout id-token: write # Required for AWS OIDC jobs: diff --git a/SECURITY.md b/SECURITY.md index 301fe8a1f..22882775e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,6 +3,7 @@ At Flowise, we prioritize security and continuously work to safeguard our systems. However, vulnerabilities can still exist. If you identify a security issue, please report it to us so we can address it promptly. Your cooperation helps us better protect our platform and users. ### Scope + - Flowise Cloud: cloud.flowiseai.com - Public Flowise Repositories @@ -31,7 +32,6 @@ At Flowise, we prioritize security and continuously work to safeguard our system - Known vulnerabilities in used libraries (unless exploitability can be proven) - Static application security testing findings - ### Reporting Guidelines - Submit your findings to https://github.com/FlowiseAI/Flowise/security @@ -46,9 +46,10 @@ At Flowise, we prioritize security and continuously work to safeguard our system ### Disclosure Terms -The Flowise team believes that transparency is important and public bug bounty reports are a valuable source of knowledge for bug bounty researchers. However, the Flowise team may have legitimate reasons not to disclose vulnerabilities. +The Flowise team believes that transparency is important and public bug bounty reports are a valuable source of knowledge for bug bounty researchers. However, the Flowise team may have legitimate reasons not to disclose vulnerabilities. Do not discuss or disclose vulnerability information without prior written consent. If you plan on presenting your research, please share a draft with us at least 45 days in advance for review. Avoid including: + - Data from any Flowise customer projects - Flowise user/customer information - Details about Flowise employees, contractors, or partners @@ -63,7 +64,7 @@ We will validate submissions within the below timelines. | Medium | 15 business days | | Low | 15 business days | -Your report will be kept *confidential*, and your details will not be shared without your consent. The Flowise team will triage and adjust severity or CVSS score if necessary. +Your report will be kept _confidential_, and your details will not be shared without your consent. The Flowise team will triage and adjust severity or CVSS score if necessary. We appreciate your efforts in helping us maintain a secure platform and look forward to working together to resolve any issues responsibly. ### Remediation @@ -72,15 +73,16 @@ Once the report has been verified, the Flowise team will plan the remediation st Below is the estimated time to remediate the triaged security reports. | Triaged Severity | Estimated Time to Remediate | -| ---------------------- | ---------------- | -| Critical | 30 business days | -| High | 60 business days | -| Medium | 90 business days | +| ---------------- | --------------------------- | +| Critical | 30 business days | +| High | 60 business days | +| Medium | 90 business days | ### Public Disclosure Timeline Public Disclosure occurs exactly 30 days after the next official release that includes the security patch. This period gives Flowise users a time to adopt the patched version before technical vulnerability details are made public, mitigating the risk of immediate post-disclosure exploitation. #### Reaching out to the Security team + To report a new vulnerability, please submit a Github security Security Advisory report. -If you have any questions or concerns about the existing Security Advisory, please contact security-team@flowiseai.com. \ No newline at end of file +If you have any questions or concerns about the existing Security Advisory, please contact security-team@flowiseai.com. diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 73bf28bc3..7b8845a01 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1198,6 +1198,42 @@ export const mapMimeTypeToExt = (mimeType: string) => { } } +/** + * MIME types allowed for full file upload (chatflow config). + * Server validates stored allowedUploadFileTypes against this list to prevent + * malicious clients from allowing executables or other dangerous types. + * Uses a Set for O(1) lookups and to make the unique allowed set explicit. + */ +export const ALLOWED_UPLOAD_MIME_TYPES: ReadonlySet = new Set([ + 'text/css', + 'text/csv', + 'text/html', + 'application/json', + 'text/markdown', + 'application/x-yaml', + 'application/pdf', + 'application/sql', + 'text/plain', + 'application/xml', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' +]) + +/** + * Returns true if the MIME type is allowed for file upload config. + * Must be in ALLOWED_UPLOAD_MIME_TYPES and have a mapping in mapMimeTypeToExt. + * @param {string} mime + * @returns {boolean} + */ +export const isAllowedUploadMimeType = (mime: string): boolean => { + if (!mime || typeof mime !== 'string') return false + const trimmed = mime.trim() + if (!trimmed) return false + return ALLOWED_UPLOAD_MIME_TYPES.has(trimmed) && mapMimeTypeToExt(trimmed) !== '' +} + // remove invalid markdown image pattern: ![]() export const removeInvalidImageMarkdown = (output: string): string => { return typeof output === 'string' ? output.replace(/!\[.*?\]\((?!https?:\/\/).*?\)/g, '') : output diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index 8fdbd3631..ca248a94b 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -1,5 +1,5 @@ import path from 'path' -import { mapMimeTypeToExt, getUserHome } from './utils' +import { getUserHome, isAllowedUploadMimeType, mapMimeTypeToExt } from './utils' /** * Validates if a string is a valid UUID v4 @@ -149,6 +149,17 @@ export const validateMimeTypeAndExtensionMatch = (filename: string, mimetype: st } } +/** + * Filters an array of MIME type strings to only those allowed for file upload config. + * Used when sanitizing chatbotConfig.allowedUploadFileTypes to prevent malicious values. + * @param {string[]} mimeTypes Raw MIME types (e.g. from splitting comma-separated config) + * @returns {string[]} Only MIME types that pass isAllowedUploadMimeType + */ +export const filterAllowedUploadMimeTypes = (mimeTypes: string[]): string[] => { + if (!Array.isArray(mimeTypes)) return [] + return mimeTypes.map((m) => (typeof m === 'string' ? m.trim() : '')).filter((m) => m !== '' && isAllowedUploadMimeType(m)) +} + /** * Get allowed base directories for vector store operations * @returns {string[]} Array of allowed base directory paths diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index 6dd40c72c..e28fe278b 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -15,6 +15,7 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' import documentStoreService from '../../services/documentstore' import { constructGraphs, getAppVersion, getEndingNodes, getTelemetryFlowObj, isFlowValidForStream } from '../../utils' +import { sanitizeAllowedUploadMimeTypesFromConfig } from '../../utils/fileValidation' import { containsBase64File, updateFlowDataWithFilePaths } from '../../utils/fileRepository' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { utilGetUploadsConfig } from '../../utils/getUploadsConfig' @@ -351,6 +352,21 @@ const updateChatflow = async ( } else { updateChatFlow.type = chatflow.type } + if (updateChatFlow.chatbotConfig) { + try { + const parsed = JSON.parse(updateChatFlow.chatbotConfig) as ICommonObject + if (parsed?.fullFileUpload?.allowedUploadFileTypes !== undefined) { + const current = parsed.fullFileUpload.allowedUploadFileTypes + const sanitized = sanitizeAllowedUploadMimeTypesFromConfig(typeof current === 'string' ? current : String(current ?? '')) + parsed.fullFileUpload.allowedUploadFileTypes = sanitized + updateChatFlow.chatbotConfig = JSON.stringify(parsed) + } + } catch (error) { + const message = getErrorMessage(error) + logger.error(`[server]: Invalid chatbotConfig JSON in updateChatflow: ${message}`) + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Invalid chatbotConfig: ${message}`) + } + } const newDbChatflow = appServer.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) await _checkAndUpdateDocumentStoreUsage(newDbChatflow, chatflow.workspaceId) const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).save(newDbChatflow) diff --git a/packages/server/src/utils/fileValidation.ts b/packages/server/src/utils/fileValidation.ts index 6c000e052..c2ef6c1c8 100644 --- a/packages/server/src/utils/fileValidation.ts +++ b/packages/server/src/utils/fileValidation.ts @@ -1,4 +1,4 @@ -import { validateMimeTypeAndExtensionMatch } from 'flowise-components' +import { filterAllowedUploadMimeTypes, validateMimeTypeAndExtensionMatch } from 'flowise-components' import { InternalFlowiseError } from '../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import { getErrorMessage } from '../errors/utils' @@ -25,3 +25,20 @@ export function validateFileMimeTypeAndExtensionMatch(filename: string, mimetype throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error)) } } + +/** + * Sanitizes the allowedUploadFileTypes string from chatbotConfig by keeping only + * MIME types that are in the server allow list. Removes any type not in the list + * (e.g. executables) to prevent malicious clients from persisting dangerous types. + * + * @param {string} allowedTypesString Comma-separated MIME types from config + * @returns {string} Comma-separated string of allowed MIME types only + */ +export function sanitizeAllowedUploadMimeTypesFromConfig(allowedTypesString: string): string { + if (typeof allowedTypesString !== 'string') return '' + const parts = allowedTypesString + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + return filterAllowedUploadMimeTypes(parts).join(',') +}