Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fix: CORS-related issues #5310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
0xi4o wants to merge 11 commits into main
base: main
Choose a base branch
Loading
from fix/cors
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
7ca2a25
remove allowed origins from public chatbot config response
0xi4o Oct 10, 2025
349df72
update how domains are validated in cors middleware
0xi4o Oct 13, 2025
c0d0305
fix: delete correct allowed domains keys in public chatbot config end...
0xi4o Oct 14, 2025
17e5722
fix: cors substring issue
0xi4o Oct 14, 2025
f03bd60
fix: remove cors origins fallback
0xi4o Oct 16, 2025
f22ab93
fix: error when cors origins is not defined
0xi4o Oct 17, 2025
42c8598
fix: update how cors setting is parsed and used
0xi4o Oct 22, 2025
1ed315e
fix: update how cors setting is parsed and used
0xi4o Oct 22, 2025
c01c33b
fix: address pr comments
0xi4o Oct 29, 2025
8d0eaa0
Merge branch 'main' of github.com:FlowiseAI/Flowise into fix/cors
0xi4o Oct 31, 2025
399ac37
fix: use workspaceId if available in cors middleware
0xi4o Oct 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some comments aren't visible on the classic Files Changed page.

2 changes: 2 additions & 0 deletions packages/server/src/routes/predictions/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getMulterStorage } from '../../utils'

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
router.post(
['/', '/:id'],
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/services/chatflows/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
} catch (e) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error parsing Chatbot Config for Chatflow ${chatflowId}`)
Expand Down
63 changes: 53 additions & 10 deletions packages/server/src/utils/XSS.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express'
import sanitizeHtml from 'sanitize-html'
import { isPredictionRequest, extractChatflowId, validateChatflowDomain } from './domainValidation'

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

export function getAllowedCorsOrigins(): string {
// Expects FQDN separated by commas, otherwise nothing or * for all.
return process.env.CORS_ORIGINS ?? '*'
// Expects FQDN separated by commas, otherwise nothing.
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 {
const corsOptions = {
origin: function (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) {
const allowedOrigins = getAllowedCorsOrigins()
if (!origin || allowedOrigins == '*' || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(null, false)
return (req: any, callback: (err: Error | null, options?: any) => void) => {
const corsOptions = {
origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
const allowedOrigins = getAllowedCorsOrigins()
const isPredictionReq = isPredictionRequest(req.url)

if (!origin || allowedOrigins === '*') {
await checkRequestType(isPredictionReq, req, origin, originCallback)
} else {
const allowedOriginsList = parseAllowedOrigins(allowedOrigins)
if (origin && allowedOriginsList.includes(origin)) {
await checkRequestType(isPredictionReq, req, origin, originCallback)
} else {
originCallback(null, false)
}
}
}
}
callback(null, corsOptions)
}
}
Comment on lines 27 to +62
Copy link

@coderabbitai coderabbitai bot Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: global CORS gate blocks chatflow-level validation for predictions

With allowedOrigins unset (''), any request with an Origin header is denied before checkRequestType runs. This breaks prediction requests that rely on per-chatflow allowedOrigins.

Apply this diff to evaluate chatflow rules first for prediction requests, and use OR semantics with the global allowlist:

 export function getCorsOptions(): any {
- return (req: any, callback: (err: Error | null, options?: any) => void) => {
+ return (req: any, callback: (err: Error | null, options?: any) => void) => {
 const corsOptions = {
- origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
- const allowedOrigins = getAllowedCorsOrigins()
- const isPredictionReq = isPredictionRequest(req.url)
-
- if (!origin || allowedOrigins === '*') {
- await checkRequestType(isPredictionReq, req, origin, originCallback)
- } else {
- const allowedOriginsList = parseAllowedOrigins(allowedOrigins)
- if (origin && allowedOriginsList.includes(origin)) {
- await checkRequestType(isPredictionReq, req, origin, originCallback)
- } else {
- originCallback(null, false)
- }
- }
- }
+ origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => {
+ const allowedOrigins = getAllowedCorsOrigins()
+ const isPredictionReq = isPredictionRequest(req.url)
+ const allowedList = parseAllowedOrigins(allowedOrigins)
+ 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 chatflowAllowed = await (async () => {
+ const chatflowId = extractChatflowId(req.url)
+ return chatflowId ? await validateChatflowDomain(chatflowId, originLc, req.user?.activeWorkspaceId) : true
+ })()
+ return originCallback(null, globallyAllowed || chatflowAllowed)
+ }
+
+ // Non-prediction: rely on global policy only
+ return originCallback(null, globallyAllowed)
+ }
 }
 callback(null, corsOptions)
 }
 }

Also normalize the comparison by lowercasing origin before includes().

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/server/src/utils/XSS.ts around lines 41-62, the origin handler
currently blocks prediction requests when the global allowedOrigins is unset and
runs the global check before the chatflow-level check; change the logic so that
for prediction requests you evaluate the chatflow rule (checkRequestType) first
and accept if it allows the request, otherwise fall back to the global
allowlist; when validating against lists normalize origin to lowercase and
compare against a lowercased allowedOrigins list, and treat the global allowlist
OR the chatflow allowlist as permissive (i.e., allow if either permits) so
prediction requests aren’t rejected prematurely.


async function checkRequestType(
isPredictionReq: boolean,
req: any,
origin: string | undefined,
originCallback: (err: Error | null, allow?: boolean) => void
) {
if (isPredictionReq) {
const chatflowId = extractChatflowId(req.url)
if (chatflowId && origin) {
const isAllowed = await validateChatflowDomain(chatflowId, origin, req.user?.activeWorkspaceId)

originCallback(null, isAllowed)
} else {
originCallback(null, true)
}
} else {
originCallback(null, true)
}
return corsOptions
}

export function getAllowedIframeOrigins(): string {
Expand Down
109 changes: 109 additions & 0 deletions packages/server/src/utils/domainValidation.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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 }
Loading

AltStyle によって変換されたページ (->オリジナル) /