Skip to content

Commit fd7fc2f

Browse files
authored
Fix: CORS-related issues (#5310)
* remove allowed origins from public chatbot config response * update how domains are validated in cors middleware * fix: delete correct allowed domains keys in public chatbot config endpoint * fix: cors substring issue * fix: remove cors origins fallback * fix: error when cors origins is not defined * fix: update how cors setting is parsed and used * fix: update how cors setting is parsed and used * fix: address pr comments * fix: use workspaceId if available in cors middleware * fix: global cors blocks chatflow-level validation for predictions * fix: add error handling to domain validation
1 parent a92f7df commit fd7fc2f

File tree

4 files changed

+162
-10
lines changed

4 files changed

+162
-10
lines changed

packages/server/src/routes/predictions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getMulterStorage } from '../../utils'
44

55
const router = express.Router()
66

7+
// NOTE: extractChatflowId function in XSS.ts extracts the chatflow ID from the prediction URL.
8+
// It assumes the URL format is /prediction/{chatflowId}. Make sure to update the function if the URL format changes.
79
// CREATE
810
router.post(
911
['/', '/:id'],

packages/server/src/services/chatflows/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ const getSinglePublicChatbotConfig = async (chatflowId: string): Promise<any> =>
378378
}
379379
})
380380
}
381+
delete parsedConfig.allowedOrigins
382+
delete parsedConfig.allowedOriginsError
381383
return { ...parsedConfig, uploads: uploadsConfig, flowData: dbResponse.flowData, isTTSEnabled }
382384
} catch (e) {
383385
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error parsing Chatbot Config for Chatflow ${chatflowId}`)

packages/server/src/utils/XSS.ts

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

45
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
56
// decoding is necessary as the url is encoded by the browser
@@ -20,22 +21,60 @@ export function sanitizeMiddleware(req: Request, res: Response, next: NextFuncti
2021
}
2122

2223
export function getAllowedCorsOrigins(): string {
23-
// Expects FQDN separated by commas, otherwise nothing or * for all.
24-
return process.env.CORS_ORIGINS ?? '*'
24+
// Expects FQDN separated by commas, otherwise nothing.
25+
return process.env.CORS_ORIGINS ?? ''
26+
}
27+
28+
function parseAllowedOrigins(allowedOrigins: string): string[] {
29+
if (!allowedOrigins) {
30+
return []
31+
}
32+
if (allowedOrigins === '*') {
33+
return ['*']
34+
}
35+
return allowedOrigins
36+
.split(',')
37+
.map((origin) => origin.trim().toLowerCase())
38+
.filter((origin) => origin.length > 0)
2539
}
2640

2741
export function getCorsOptions(): any {
28-
const corsOptions = {
29-
origin: function (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) {
30-
const allowedOrigins = getAllowedCorsOrigins()
31-
if (!origin || allowedOrigins == '*' || allowedOrigins.indexOf(origin) !== -1) {
32-
callback(null, true)
33-
} else {
34-
callback(null, false)
42+
return (req: any, callback: (err: Error | null, options?: any) => void) => {
43+
const corsOptions = {
44+
origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
45+
const allowedOrigins = getAllowedCorsOrigins()
46+
const isPredictionReq = isPredictionRequest(req.url)
47+
const allowedList = parseAllowedOrigins(allowedOrigins)
48+
const originLc = origin?.toLowerCase()
49+
50+
// Always allow no-Origin requests (same-origin, server-to-server)
51+
if (!originLc) return originCallback(null, true)
52+
53+
// Global allow: '*' or exact match
54+
const globallyAllowed = allowedOrigins === '*' || allowedList.includes(originLc)
55+
56+
if (isPredictionReq) {
57+
// Per-chatflow allowlist OR globally allowed
58+
const chatflowId = extractChatflowId(req.url)
59+
let chatflowAllowed = false
60+
if (chatflowId) {
61+
try {
62+
chatflowAllowed = await validateChatflowDomain(chatflowId, originLc, req.user?.activeWorkspaceId)
63+
} catch (error) {
64+
// Log error and deny on failure
65+
console.error('Domain validation error:', error)
66+
chatflowAllowed = false
67+
}
68+
}
69+
return originCallback(null, globallyAllowed || chatflowAllowed)
70+
}
71+
72+
// Non-prediction: rely on global policy only
73+
return originCallback(null, globallyAllowed)
3574
}
3675
}
76+
callback(null, corsOptions)
3777
}
38-
return corsOptions
3978
}
4079

4180
export function getAllowedIframeOrigins(): string {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { isValidUUID } from 'flowise-components'
2+
import chatflowsService from '../services/chatflows'
3+
import logger from './logger'
4+
5+
/**
6+
* Validates if the origin is allowed for a specific chatflow
7+
* @param chatflowId - The chatflow ID to validate against
8+
* @param origin - The origin URL to validate
9+
* @param workspaceId - Optional workspace ID for enterprise features
10+
* @returns Promise<boolean> - True if domain is allowed, false otherwise
11+
*/
12+
async function validateChatflowDomain(chatflowId: string, origin: string, workspaceId?: string): Promise<boolean> {
13+
try {
14+
if (!chatflowId || !isValidUUID(chatflowId)) {
15+
throw new Error('Invalid chatflowId format - must be a valid UUID')
16+
}
17+
18+
const chatflow = workspaceId
19+
? await chatflowsService.getChatflowById(chatflowId, workspaceId)
20+
: await chatflowsService.getChatflowById(chatflowId)
21+
22+
if (!chatflow?.chatbotConfig) {
23+
return true
24+
}
25+
26+
const config = JSON.parse(chatflow.chatbotConfig)
27+
28+
// If no allowed origins configured or first entry is empty, allow all
29+
if (!config.allowedOrigins?.length || config.allowedOrigins[0] === '') {
30+
return true
31+
}
32+
33+
const originHost = new URL(origin).host
34+
const isAllowed = config.allowedOrigins.some((domain: string) => {
35+
try {
36+
const allowedOrigin = new URL(domain).host
37+
return originHost === allowedOrigin
38+
} catch (error) {
39+
logger.warn(`Invalid domain format in allowedOrigins: ${domain}`)
40+
return false
41+
}
42+
})
43+
44+
return isAllowed
45+
} catch (error) {
46+
logger.error(`Error validating domain for chatflow ${chatflowId}:`, error)
47+
return false
48+
}
49+
}
50+
51+
// NOTE: This function extracts the chatflow ID from a prediction URL.
52+
// It assumes the URL format is /prediction/{chatflowId}.
53+
/**
54+
* Extracts chatflow ID from prediction URL
55+
* @param url - The request URL
56+
* @returns string | null - The chatflow ID or null if not found
57+
*/
58+
function extractChatflowId(url: string): string | null {
59+
try {
60+
const urlParts = url.split('/')
61+
const predictionIndex = urlParts.indexOf('prediction')
62+
63+
if (predictionIndex !== -1 && urlParts.length > predictionIndex + 1) {
64+
const chatflowId = urlParts[predictionIndex + 1]
65+
// Remove query parameters if present
66+
return chatflowId.split('?')[0]
67+
}
68+
69+
return null
70+
} catch (error) {
71+
logger.error('Error extracting chatflow ID from URL:', error)
72+
return null
73+
}
74+
}
75+
76+
/**
77+
* Validates if a request is a prediction request
78+
* @param url - The request URL
79+
* @returns boolean - True if it's a prediction request
80+
*/
81+
function isPredictionRequest(url: string): boolean {
82+
return url.includes('/prediction/')
83+
}
84+
85+
/**
86+
* Get the custom error message for unauthorized origin
87+
* @param chatflowId - The chatflow ID
88+
* @param workspaceId - Optional workspace ID
89+
* @returns Promise<string> - Custom error message or default
90+
*/
91+
async function getUnauthorizedOriginError(chatflowId: string, workspaceId?: string): Promise<string> {
92+
try {
93+
const chatflow = workspaceId
94+
? await chatflowsService.getChatflowById(chatflowId, workspaceId)
95+
: await chatflowsService.getChatflowById(chatflowId)
96+
97+
if (chatflow?.chatbotConfig) {
98+
const config = JSON.parse(chatflow.chatbotConfig)
99+
return config.allowedOriginsError || 'This site is not allowed to access this chatbot'
100+
}
101+
102+
return 'This site is not allowed to access this chatbot'
103+
} catch (error) {
104+
logger.error(`Error getting unauthorized origin error for chatflow ${chatflowId}:`, error)
105+
return 'This site is not allowed to access this chatbot'
106+
}
107+
}
108+
109+
export { isPredictionRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError }

0 commit comments

Comments
 (0)