mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-22 09:01:09 +03:00
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
This commit is contained in:
@@ -4,6 +4,8 @@ import { getMulterStorage } from '../../utils'
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
// NOTE: extractChatflowId function in XSS.ts extracts the chatflow ID from the prediction URL.
|
||||||
|
// It assumes the URL format is /prediction/{chatflowId}. Make sure to update the function if the URL format changes.
|
||||||
// CREATE
|
// CREATE
|
||||||
router.post(
|
router.post(
|
||||||
['/', '/:id'],
|
['/', '/:id'],
|
||||||
|
|||||||
@@ -378,6 +378,8 @@ const getSinglePublicChatbotConfig = async (chatflowId: string): Promise<any> =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
delete parsedConfig.allowedOrigins
|
||||||
|
delete parsedConfig.allowedOriginsError
|
||||||
return { ...parsedConfig, uploads: uploadsConfig, flowData: dbResponse.flowData, isTTSEnabled }
|
return { ...parsedConfig, uploads: uploadsConfig, flowData: dbResponse.flowData, isTTSEnabled }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error parsing Chatbot Config for Chatflow ${chatflowId}`)
|
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error parsing Chatbot Config for Chatflow ${chatflowId}`)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express'
|
import { Request, Response, NextFunction } from 'express'
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
import { isPredictionRequest, extractChatflowId, validateChatflowDomain } from './domainValidation'
|
||||||
|
|
||||||
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
|
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||||
// decoding is necessary as the url is encoded by the browser
|
// decoding is necessary as the url is encoded by the browser
|
||||||
@@ -20,22 +21,60 @@ export function sanitizeMiddleware(req: Request, res: Response, next: NextFuncti
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAllowedCorsOrigins(): string {
|
export function getAllowedCorsOrigins(): string {
|
||||||
// Expects FQDN separated by commas, otherwise nothing or * for all.
|
// Expects FQDN separated by commas, otherwise nothing.
|
||||||
return process.env.CORS_ORIGINS ?? '*'
|
return process.env.CORS_ORIGINS ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllowedOrigins(allowedOrigins: string): string[] {
|
||||||
|
if (!allowedOrigins) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (allowedOrigins === '*') {
|
||||||
|
return ['*']
|
||||||
|
}
|
||||||
|
return allowedOrigins
|
||||||
|
.split(',')
|
||||||
|
.map((origin) => origin.trim().toLowerCase())
|
||||||
|
.filter((origin) => origin.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCorsOptions(): any {
|
export function getCorsOptions(): any {
|
||||||
const corsOptions = {
|
return (req: any, callback: (err: Error | null, options?: any) => void) => {
|
||||||
origin: function (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) {
|
const corsOptions = {
|
||||||
const allowedOrigins = getAllowedCorsOrigins()
|
origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
|
||||||
if (!origin || allowedOrigins == '*' || allowedOrigins.indexOf(origin) !== -1) {
|
const allowedOrigins = getAllowedCorsOrigins()
|
||||||
callback(null, true)
|
const isPredictionReq = isPredictionRequest(req.url)
|
||||||
} else {
|
const allowedList = parseAllowedOrigins(allowedOrigins)
|
||||||
callback(null, false)
|
const originLc = origin?.toLowerCase()
|
||||||
|
|
||||||
|
// Always allow no-Origin requests (same-origin, server-to-server)
|
||||||
|
if (!originLc) return originCallback(null, true)
|
||||||
|
|
||||||
|
// Global allow: '*' or exact match
|
||||||
|
const globallyAllowed = allowedOrigins === '*' || allowedList.includes(originLc)
|
||||||
|
|
||||||
|
if (isPredictionReq) {
|
||||||
|
// Per-chatflow allowlist OR globally allowed
|
||||||
|
const chatflowId = extractChatflowId(req.url)
|
||||||
|
let chatflowAllowed = false
|
||||||
|
if (chatflowId) {
|
||||||
|
try {
|
||||||
|
chatflowAllowed = await validateChatflowDomain(chatflowId, originLc, req.user?.activeWorkspaceId)
|
||||||
|
} catch (error) {
|
||||||
|
// Log error and deny on failure
|
||||||
|
console.error('Domain validation error:', error)
|
||||||
|
chatflowAllowed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originCallback(null, globallyAllowed || chatflowAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-prediction: rely on global policy only
|
||||||
|
return originCallback(null, globallyAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
callback(null, corsOptions)
|
||||||
}
|
}
|
||||||
return corsOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllowedIframeOrigins(): string {
|
export function getAllowedIframeOrigins(): string {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { isValidUUID } from 'flowise-components'
|
||||||
|
import chatflowsService from '../services/chatflows'
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if the origin is allowed for a specific chatflow
|
||||||
|
* @param chatflowId - The chatflow ID to validate against
|
||||||
|
* @param origin - The origin URL to validate
|
||||||
|
* @param workspaceId - Optional workspace ID for enterprise features
|
||||||
|
* @returns Promise<boolean> - True if domain is allowed, false otherwise
|
||||||
|
*/
|
||||||
|
async function validateChatflowDomain(chatflowId: string, origin: string, workspaceId?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!chatflowId || !isValidUUID(chatflowId)) {
|
||||||
|
throw new Error('Invalid chatflowId format - must be a valid UUID')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatflow = workspaceId
|
||||||
|
? await chatflowsService.getChatflowById(chatflowId, workspaceId)
|
||||||
|
: await chatflowsService.getChatflowById(chatflowId)
|
||||||
|
|
||||||
|
if (!chatflow?.chatbotConfig) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(chatflow.chatbotConfig)
|
||||||
|
|
||||||
|
// If no allowed origins configured or first entry is empty, allow all
|
||||||
|
if (!config.allowedOrigins?.length || config.allowedOrigins[0] === '') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const originHost = new URL(origin).host
|
||||||
|
const isAllowed = config.allowedOrigins.some((domain: string) => {
|
||||||
|
try {
|
||||||
|
const allowedOrigin = new URL(domain).host
|
||||||
|
return originHost === allowedOrigin
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Invalid domain format in allowedOrigins: ${domain}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return isAllowed
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error validating domain for chatflow ${chatflowId}:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function extracts the chatflow ID from a prediction URL.
|
||||||
|
// It assumes the URL format is /prediction/{chatflowId}.
|
||||||
|
/**
|
||||||
|
* Extracts chatflow ID from prediction URL
|
||||||
|
* @param url - The request URL
|
||||||
|
* @returns string | null - The chatflow ID or null if not found
|
||||||
|
*/
|
||||||
|
function extractChatflowId(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const urlParts = url.split('/')
|
||||||
|
const predictionIndex = urlParts.indexOf('prediction')
|
||||||
|
|
||||||
|
if (predictionIndex !== -1 && urlParts.length > predictionIndex + 1) {
|
||||||
|
const chatflowId = urlParts[predictionIndex + 1]
|
||||||
|
// Remove query parameters if present
|
||||||
|
return chatflowId.split('?')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error extracting chatflow ID from URL:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a request is a prediction request
|
||||||
|
* @param url - The request URL
|
||||||
|
* @returns boolean - True if it's a prediction request
|
||||||
|
*/
|
||||||
|
function isPredictionRequest(url: string): boolean {
|
||||||
|
return url.includes('/prediction/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the custom error message for unauthorized origin
|
||||||
|
* @param chatflowId - The chatflow ID
|
||||||
|
* @param workspaceId - Optional workspace ID
|
||||||
|
* @returns Promise<string> - Custom error message or default
|
||||||
|
*/
|
||||||
|
async function getUnauthorizedOriginError(chatflowId: string, workspaceId?: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const chatflow = workspaceId
|
||||||
|
? await chatflowsService.getChatflowById(chatflowId, workspaceId)
|
||||||
|
: await chatflowsService.getChatflowById(chatflowId)
|
||||||
|
|
||||||
|
if (chatflow?.chatbotConfig) {
|
||||||
|
const config = JSON.parse(chatflow.chatbotConfig)
|
||||||
|
return config.allowedOriginsError || 'This site is not allowed to access this chatbot'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'This site is not allowed to access this chatbot'
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error getting unauthorized origin error for chatflow ${chatflowId}:`, error)
|
||||||
|
return 'This site is not allowed to access this chatbot'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isPredictionRequest, extractChatflowId, validateChatflowDomain, getUnauthorizedOriginError }
|
||||||
Reference in New Issue
Block a user