-
-
Notifications
You must be signed in to change notification settings - Fork 23k
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
Fix: CORS-related issues #5310
Changes from all commits
7ca2a25
349df72
c0d0305
17e5722
f03bd60
f22ab93
42c8598
1ed315e
c01c33b
8d0eaa0
399ac37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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().
🤖 Prompt for AI Agents |
||
|
|
||
| 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 { | ||
|
|
||
| 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('/') | ||
0xi4o marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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/') | ||
| } | ||
HenryHengZJ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 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 } | ||