Skip to content

Commit d2acf4c

Browse files
christopherholland-workdaychristopherholland-workdayyau-wd0xi4o
authored
Hardcoded CORS wildcard on TTS endpoint enables cross-origin credential abuse from any webpage (#5901)
* Stop text-to-speach endpoint from accepting arbitrary creds * Stop text-to-speach endpoint from accepting arbitrary creds * Hardcoded CORS wildcard on TTS endpoint enables cross-origin credential abuse from any webpage * add: allow tts in domain validation --------- Co-authored-by: christopherholland-workday <christopher.holland+evisort@workday.com> Co-authored-by: yau-wd <yau.ong@workday.com> Co-authored-by: Ilango Rajagopal <ilango.rajagopal@flowiseai.com>
1 parent 34cf285 commit d2acf4c

3 files changed

Lines changed: 22 additions & 6 deletions

File tree

packages/server/src/controllers/text-to-speech/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ const generateTextToSpeech = async (req: Request, res: Response) => {
8989
res.setHeader('Content-Type', 'text/event-stream')
9090
res.setHeader('Cache-Control', 'no-cache')
9191
res.setHeader('Connection', 'keep-alive')
92-
res.setHeader('Access-Control-Allow-Origin', '*')
93-
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control')
9492

9593
const appServer = getRunningExpressApp()
9694
const options = {

packages/server/src/utils/XSS.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response, NextFunction } from 'express'
22
import sanitizeHtml from 'sanitize-html'
3-
import { extractChatflowId, validateChatflowDomain, isPublicChatflowRequest } from './domainValidation'
3+
import { extractChatflowId, validateChatflowDomain, isPublicChatflowRequest, isTTSGenerateRequest } from './domainValidation'
44

55
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
66
// decoding is necessary as the url is encoded by the browser
@@ -44,6 +44,7 @@ export function getCorsOptions(): any {
4444
origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
4545
const allowedOrigins = getAllowedCorsOrigins()
4646
const isPublicChatflowReq = isPublicChatflowRequest(req.url)
47+
const isTTSReq = isTTSGenerateRequest(req.url)
4748
const allowedList = parseAllowedOrigins(allowedOrigins)
4849
const originLc = origin?.toLowerCase()
4950

@@ -53,9 +54,10 @@ export function getCorsOptions(): any {
5354
// Global allow: '*' or exact match
5455
const globallyAllowed = allowedOrigins === '*' || allowedList.includes(originLc)
5556

56-
if (isPublicChatflowReq) {
57+
if (isPublicChatflowReq || isTTSReq) {
5758
// Per-chatflow allowlist OR globally allowed
58-
const chatflowId = extractChatflowId(req.url)
59+
// TTS generate passes chatflowId in the request body, not the URL path
60+
const chatflowId = isTTSReq ? req.body?.chatflowId : extractChatflowId(req.url)
5961
let chatflowAllowed = false
6062
if (chatflowId) {
6163
try {
@@ -65,6 +67,9 @@ export function getCorsOptions(): any {
6567
console.error('Domain validation error:', error)
6668
chatflowAllowed = false
6769
}
70+
} else if (isTTSReq) {
71+
// OPTIONS preflight has no body — allow it through so the actual POST can be validated with chatflowId
72+
chatflowAllowed = true
6873
}
6974
return originCallback(null, globallyAllowed || chatflowAllowed)
7075
}

packages/server/src/utils/domainValidation.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import logger from './logger'
99
// /chatflows-streaming/{chatflowId}
1010
const ALLOWED_SLUGS = ['/prediction/', '/public-chatbotConfig/', '/chatflows-streaming/']
1111

12+
// The TTS generate endpoint passes chatflowId in the request body, not the URL path
13+
const TTS_GENERATE_PATH = '/api/v1/text-to-speech/generate'
14+
1215
/**
1316
* Validates if the origin is allowed for a specific chatflow
1417
* @param chatflowId - The chatflow ID to validate against
@@ -105,6 +108,16 @@ function isPublicChatflowRequest(url: string): boolean {
105108
return extractSlugFromUrl(url) !== null
106109
}
107110

111+
/**
112+
* Checks if the request is for the TTS generate endpoint.
113+
* This endpoint passes chatflowId in the request body rather than the URL path.
114+
* @param url - The request URL
115+
* @returns boolean - True if it's the TTS generate endpoint
116+
*/
117+
function isTTSGenerateRequest(url: string): boolean {
118+
return url.split('?')[0] === TTS_GENERATE_PATH
119+
}
120+
108121
/**
109122
* Get the custom error message for unauthorized origin
110123
* @param chatflowId - The chatflow ID
@@ -129,4 +142,4 @@ async function getUnauthorizedOriginError(chatflowId: string, workspaceId?: stri
129142
}
130143
}
131144

132-
export { isPublicChatflowRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError }
145+
export { isPublicChatflowRequest, isTTSGenerateRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError }

0 commit comments

Comments
 (0)