- 
          
- 
                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
base: main
Are you sure you want to change the base?
Fix: CORS-related issues #5310
Changes from 8 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 | ||
|  | @@ -21,21 +22,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 ?? '*' | ||
| // return process.env.CORS_ORIGINS ?? '*' | ||
| return process.env.CORS_ORIGINS as string | ||
| } | ||
|  | ||
| 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,103 @@ | ||
| 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 { | ||
| // TODO: Add workspaceId from here | ||
| const chatflow = await chatflowsService.getChatflowById(chatflowId) | ||
|  | ||
| if (!chatflow?.chatbotConfig) { | ||
| logger.info(`No chatbotConfig found for chatflow ${chatflowId}, allowing domain`) | ||
|         
                  0xi4o marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| 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] === '') { | ||
| logger.info(`No domain restrictions configured for chatflow ${chatflowId}`) | ||
| 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 | ||
| } | ||
| }) | ||
|  | ||
| logger.info(`Domain validation for ${origin} against chatflow ${chatflowId}: ${isAllowed}`) | ||
| return isAllowed | ||
| } catch (error) { | ||
| logger.error(`Error validating domain for chatflow ${chatflowId}:`, error) | ||
| return false | ||
| } | ||
| } | ||
|  | ||
| /** | ||
| * 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 { | ||
| // TODO: Add workspaceId from here | ||
| const chatflow = 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 } | ||
Uh oh!
There was an error while loading. Please reload this page.