From f174afdf5203085e854dc8af832cf54ef4372522 Mon Sep 17 00:00:00 2001 From: christopherholland-workday Date: Wed, 4 Mar 2026 11:07:56 -0800 Subject: [PATCH 1/4] Stop text-to-speach endpoint from accepting arbitrary creds --- packages/components/cookies.txt | 7 +++++++ .../server/src/controllers/text-to-speech/index.ts | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/components/cookies.txt diff --git a/packages/components/cookies.txt b/packages/components/cookies.txt new file mode 100644 index 00000000000..838518d7d6c --- /dev/null +++ b/packages/components/cookies.txt @@ -0,0 +1,7 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 0 connect.sid s%3Acwjbi1guxLcnar0s1aVCP5bMBvSsyX4f.VHFJfS8WHyPbC2xM8Src7AU9MAB2kF15w%2F%2FY7G2wwIc +#HttpOnly_localhost FALSE / FALSE 0 refreshToken eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwZDQxNjQ2LTk1YmYtNDM4ZS1iZGE2LWU3ZjlkY2QyZjcxZSIsInVzZXJuYW1lIjoiQ2hyaXMgQWRtaW4iLCJtZXRhIjoiMDdjNzAwNGU2NzQ5OTYyN2YyNzVmY2FkNzBlMDZmMTg6MTlmODhjNTQzNjNmZWRjODljYjQ1MmVkZTYzN2E5ODliMDRlY2JiNDFkYzFjZTY2ZGM3YzMzNjk0MzAxYmM5MzQ2YzEzZjdiYjk5ZGI1YmFkYmY0NGM1ZTI2M2NhYzAzMTY0NjAxYWQ0YTk4NWM0OGM2YWVmMTAxOTZmN2JkOGYxYmEzMzg2NzkzODg4NDI4YjBiNDEwOGMxZTY2OTliNiIsImlhdCI6MTc3MjY1MTAzNCwibmJmIjoxNzcyNjUxMDM0LCJleHAiOjE3NzUyNDMwMzQsImF1ZCI6IkZsb3dpc2UiLCJpc3MiOiJGbG93aXNlIn0.8CVAjXXGxskieocmhfVt6mvD0hgiToketbQI2q6_vAs +#HttpOnly_localhost FALSE / FALSE 0 token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwZDQxNjQ2LTk1YmYtNDM4ZS1iZGE2LWU3ZjlkY2QyZjcxZSIsInVzZXJuYW1lIjoiQ2hyaXMgQWRtaW4iLCJtZXRhIjoiZmRjYjM4MWIxMTVjMzI1Zjc1MTBlYzE5YzdjYjM4Y2U6OTI0ZDg2YjRjMjllNTdjNDVmODYxOTNmNmVmNDQxMDEyNDkzYjhiMDkzNTZjYjY1YTJiY2RjNWZiMGJjMDMzOGJhZmNiZTUxN2I4MGNiZDg4ZjRmZGU3MWYxNjY2N2ExMjViOWJlMDE4NDIyYmMxYTAwMTVhYjNmZjQ4YjEwZWNlOGI2OGJjZmVhZjdlMjlmZGQ4OTRjYjAzMmYyMDEyMiIsImlhdCI6MTc3MjY1MTAzNCwibmJmIjoxNzcyNjUxMDM0LCJleHAiOjE3NzI2NzI2MzQsImF1ZCI6IkZsb3dpc2UiLCJpc3MiOiJGbG93aXNlIn0.rxhXXvWeTf8S3ggIpApR6NnHOK-ZyW_fJE3qAYxGb3c diff --git a/packages/server/src/controllers/text-to-speech/index.ts b/packages/server/src/controllers/text-to-speech/index.ts index af92713a87e..db5500c6c7e 100644 --- a/packages/server/src/controllers/text-to-speech/index.ts +++ b/packages/server/src/controllers/text-to-speech/index.ts @@ -3,6 +3,7 @@ import { convertTextToSpeechStream } from 'flowise-components' import { StatusCodes } from 'http-status-codes' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import chatflowsService from '../../services/chatflows' +import credentialsService from '../../services/credentials' import textToSpeechService from '../../services/text-to-speech' import { databaseEntities } from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' @@ -56,6 +57,17 @@ const generateTextToSpeech = async (req: Request, res: Response) => { voice = providerConfig.voice model = providerConfig.model } else { + // Body-supplied credentials require the caller to be authenticated + const workspaceId = req.user?.activeWorkspaceId + if (!workspaceId) { + return res.status(StatusCodes.UNAUTHORIZED).json({ message: 'Authentication required' }) + } + if (!bodyCredentialId) { + return res.status(StatusCodes.BAD_REQUEST).json({ message: 'credentialId not provided' }) + } + // Verify the credential belongs to the authenticated user's workspace — + // throws NOT_FOUND if the credential doesn't exist or belongs to another workspace + await credentialsService.getCredentialById(bodyCredentialId, workspaceId) // Use TTS config from request body provider = bodyProvider credentialId = bodyCredentialId @@ -80,8 +92,6 @@ const generateTextToSpeech = async (req: Request, res: Response) => { res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Headers', 'Cache-Control') const appServer = getRunningExpressApp() const options = { From 97d0c8eea850deb58a1972befac04865dad6ec8d Mon Sep 17 00:00:00 2001 From: christopherholland-workday Date: Wed, 4 Mar 2026 11:11:38 -0800 Subject: [PATCH 2/4] Stop text-to-speach endpoint from accepting arbitrary creds --- packages/components/cookies.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 packages/components/cookies.txt diff --git a/packages/components/cookies.txt b/packages/components/cookies.txt deleted file mode 100644 index 838518d7d6c..00000000000 --- a/packages/components/cookies.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Netscape HTTP Cookie File -# https://curl.se/docs/http-cookies.html -# This file was generated by libcurl! Edit at your own risk. - -#HttpOnly_localhost FALSE / FALSE 0 connect.sid s%3Acwjbi1guxLcnar0s1aVCP5bMBvSsyX4f.VHFJfS8WHyPbC2xM8Src7AU9MAB2kF15w%2F%2FY7G2wwIc -#HttpOnly_localhost FALSE / FALSE 0 refreshToken eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwZDQxNjQ2LTk1YmYtNDM4ZS1iZGE2LWU3ZjlkY2QyZjcxZSIsInVzZXJuYW1lIjoiQ2hyaXMgQWRtaW4iLCJtZXRhIjoiMDdjNzAwNGU2NzQ5OTYyN2YyNzVmY2FkNzBlMDZmMTg6MTlmODhjNTQzNjNmZWRjODljYjQ1MmVkZTYzN2E5ODliMDRlY2JiNDFkYzFjZTY2ZGM3YzMzNjk0MzAxYmM5MzQ2YzEzZjdiYjk5ZGI1YmFkYmY0NGM1ZTI2M2NhYzAzMTY0NjAxYWQ0YTk4NWM0OGM2YWVmMTAxOTZmN2JkOGYxYmEzMzg2NzkzODg4NDI4YjBiNDEwOGMxZTY2OTliNiIsImlhdCI6MTc3MjY1MTAzNCwibmJmIjoxNzcyNjUxMDM0LCJleHAiOjE3NzUyNDMwMzQsImF1ZCI6IkZsb3dpc2UiLCJpc3MiOiJGbG93aXNlIn0.8CVAjXXGxskieocmhfVt6mvD0hgiToketbQI2q6_vAs -#HttpOnly_localhost FALSE / FALSE 0 token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwZDQxNjQ2LTk1YmYtNDM4ZS1iZGE2LWU3ZjlkY2QyZjcxZSIsInVzZXJuYW1lIjoiQ2hyaXMgQWRtaW4iLCJtZXRhIjoiZmRjYjM4MWIxMTVjMzI1Zjc1MTBlYzE5YzdjYjM4Y2U6OTI0ZDg2YjRjMjllNTdjNDVmODYxOTNmNmVmNDQxMDEyNDkzYjhiMDkzNTZjYjY1YTJiY2RjNWZiMGJjMDMzOGJhZmNiZTUxN2I4MGNiZDg4ZjRmZGU3MWYxNjY2N2ExMjViOWJlMDE4NDIyYmMxYTAwMTVhYjNmZjQ4YjEwZWNlOGI2OGJjZmVhZjdlMjlmZGQ4OTRjYjAzMmYyMDEyMiIsImlhdCI6MTc3MjY1MTAzNCwibmJmIjoxNzcyNjUxMDM0LCJleHAiOjE3NzI2NzI2MzQsImF1ZCI6IkZsb3dpc2UiLCJpc3MiOiJGbG93aXNlIn0.rxhXXvWeTf8S3ggIpApR6NnHOK-ZyW_fJE3qAYxGb3c From 981ea4731521153ab4b380833d66145c77a9ba33 Mon Sep 17 00:00:00 2001 From: christopherholland-workday Date: Mon, 9 Mar 2026 10:19:43 -0700 Subject: [PATCH 3/4] Hardcoded CORS wildcard on TTS endpoint enables cross-origin credential abuse from any webpage --- .../server/src/controllers/text-to-speech/index.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/server/src/controllers/text-to-speech/index.ts b/packages/server/src/controllers/text-to-speech/index.ts index db5500c6c7e..4346d460269 100644 --- a/packages/server/src/controllers/text-to-speech/index.ts +++ b/packages/server/src/controllers/text-to-speech/index.ts @@ -3,7 +3,6 @@ import { convertTextToSpeechStream } from 'flowise-components' import { StatusCodes } from 'http-status-codes' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import chatflowsService from '../../services/chatflows' -import credentialsService from '../../services/credentials' import textToSpeechService from '../../services/text-to-speech' import { databaseEntities } from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' @@ -57,17 +56,6 @@ const generateTextToSpeech = async (req: Request, res: Response) => { voice = providerConfig.voice model = providerConfig.model } else { - // Body-supplied credentials require the caller to be authenticated - const workspaceId = req.user?.activeWorkspaceId - if (!workspaceId) { - return res.status(StatusCodes.UNAUTHORIZED).json({ message: 'Authentication required' }) - } - if (!bodyCredentialId) { - return res.status(StatusCodes.BAD_REQUEST).json({ message: 'credentialId not provided' }) - } - // Verify the credential belongs to the authenticated user's workspace — - // throws NOT_FOUND if the credential doesn't exist or belongs to another workspace - await credentialsService.getCredentialById(bodyCredentialId, workspaceId) // Use TTS config from request body provider = bodyProvider credentialId = bodyCredentialId From 357b9e3b625f630b2f61be5aba82694f109d0db2 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Thu, 12 Mar 2026 14:57:16 +0530 Subject: [PATCH 4/4] add: allow tts in domain validation --- packages/server/src/utils/XSS.ts | 11 ++++++++--- packages/server/src/utils/domainValidation.ts | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index a42d2d6ddf0..180e44584ad 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express' import sanitizeHtml from 'sanitize-html' -import { extractChatflowId, validateChatflowDomain, isPublicChatflowRequest } from './domainValidation' +import { extractChatflowId, validateChatflowDomain, isPublicChatflowRequest, isTTSGenerateRequest } from './domainValidation' export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { // decoding is necessary as the url is encoded by the browser @@ -44,6 +44,7 @@ export function getCorsOptions(): any { origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => { const allowedOrigins = getAllowedCorsOrigins() const isPublicChatflowReq = isPublicChatflowRequest(req.url) + const isTTSReq = isTTSGenerateRequest(req.url) const allowedList = parseAllowedOrigins(allowedOrigins) const originLc = origin?.toLowerCase() @@ -53,9 +54,10 @@ export function getCorsOptions(): any { // Global allow: '*' or exact match const globallyAllowed = allowedOrigins === '*' || allowedList.includes(originLc) - if (isPublicChatflowReq) { + if (isPublicChatflowReq || isTTSReq) { // Per-chatflow allowlist OR globally allowed - const chatflowId = extractChatflowId(req.url) + // TTS generate passes chatflowId in the request body, not the URL path + const chatflowId = isTTSReq ? req.body?.chatflowId : extractChatflowId(req.url) let chatflowAllowed = false if (chatflowId) { try { @@ -65,6 +67,9 @@ export function getCorsOptions(): any { console.error('Domain validation error:', error) chatflowAllowed = false } + } else if (isTTSReq) { + // OPTIONS preflight has no body — allow it through so the actual POST can be validated with chatflowId + chatflowAllowed = true } return originCallback(null, globallyAllowed || chatflowAllowed) } diff --git a/packages/server/src/utils/domainValidation.ts b/packages/server/src/utils/domainValidation.ts index ec4c84c6188..60372fbeff1 100644 --- a/packages/server/src/utils/domainValidation.ts +++ b/packages/server/src/utils/domainValidation.ts @@ -9,6 +9,9 @@ import logger from './logger' // /chatflows-streaming/{chatflowId} const ALLOWED_SLUGS = ['/prediction/', '/public-chatbotConfig/', '/chatflows-streaming/'] +// The TTS generate endpoint passes chatflowId in the request body, not the URL path +const TTS_GENERATE_PATH = '/api/v1/text-to-speech/generate' + /** * Validates if the origin is allowed for a specific chatflow * @param chatflowId - The chatflow ID to validate against @@ -105,6 +108,16 @@ function isPublicChatflowRequest(url: string): boolean { return extractSlugFromUrl(url) !== null } +/** + * Checks if the request is for the TTS generate endpoint. + * This endpoint passes chatflowId in the request body rather than the URL path. + * @param url - The request URL + * @returns boolean - True if it's the TTS generate endpoint + */ +function isTTSGenerateRequest(url: string): boolean { + return url.split('?')[0] === TTS_GENERATE_PATH +} + /** * Get the custom error message for unauthorized origin * @param chatflowId - The chatflow ID @@ -129,4 +142,4 @@ async function getUnauthorizedOriginError(chatflowId: string, workspaceId?: stri } } -export { isPublicChatflowRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError } +export { isPublicChatflowRequest, isTTSGenerateRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError }