Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 37 additions & 2 deletions packages/components/src/httpSecurity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@
}
}

/**
* Checks if a URL is allowed based on HTTP_ALLOW_LIST environment variable
* @param url - URL to check
* @throws Error if URL hostname is not in allow-list
*/
export function checkAllowList(url: string): void {
const httpAllowListString: string | undefined = process.env.HTTP_ALLOW_LIST
if (!httpAllowListString) {
throw new Error('Access to this host is denied: no allow-list configured')
}
const httpAllowList = httpAllowListString.split(',').map(entry => entry.trim()).filter(entry => !!entry)

Check failure on line 66 in packages/components/src/httpSecurity.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Replace `.split(',').map(entry·=>·entry.trim()).filter(entry` with `⏎········.split(',')⏎········.map((entry)·=>·entry.trim())⏎········.filter((entry)`
const urlObj = new URL(url)
const hostname = urlObj.hostname

// Only allow http and https schemes
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
throw new Error('Only http and https URLs are allowed')
}

// Strict match, or basic wildcard match e.g. *.example.com
const isAllowed = httpAllowList.some(entry => {

Check failure on line 76 in packages/components/src/httpSecurity.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Replace `entry` with `(entry)`
// wildcard matching for patterns like *.example.com
if (entry.startsWith('*.')) {
const base = entry.slice(2)
return hostname === base || hostname.endsWith('.' + base)
}
return hostname === entry
})
if (!isAllowed) {
throw new Error('Access to this host is denied by allow-list policy')
}
}

/**
* Makes a secure HTTP request that validates all URLs in redirect chains against the deny list
* @param config - Axios request configuration
Expand Down Expand Up @@ -171,7 +204,8 @@
let redirectCount = 0
let currentInit = { ...init, redirect: 'manual' as const } // Disable automatic redirects

// Validate the initial URL
// Validate the initial URL using both allow-list and deny-list
checkAllowList(currentUrl)
await checkDenyList(currentUrl)

while (redirectCount <= maxRedirects) {
Expand All @@ -198,7 +232,8 @@
// Resolve the redirect URL (handle relative URLs)
const redirectUrl = new URL(location, currentUrl).toString()

// Validate the redirect URL against the deny list
// Validate the redirect URL against the allow-list and deny list
checkAllowList(redirectUrl)
await checkDenyList(redirectUrl)

// Update current URL for next iteration
Expand Down
8 changes: 6 additions & 2 deletions packages/components/src/storageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,8 +937,12 @@
*/
const _cleanEmptyS3Folders = async (s3Client: S3Client, Bucket: string, prefix: string) => {
try {
// Skip if prefix is empty
if (!prefix) return
// Defensive: ensure prefix is a string
if (Array.isArray(prefix)) {
// Use the first value or reject, depending on business logic
prefix = prefix[0];

Check failure on line 943 in packages/components/src/storageUtils.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
}
if (typeof prefix !== 'string' || !prefix) return

// List objects in this "folder"
const listCmd = new ListObjectsV2Command({
Expand Down
36 changes: 34 additions & 2 deletions packages/server/src/controllers/chat-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@
import { utilGetChatMessage } from '../../utils/getChatMessage'
import { getPageAndLimitParams } from '../../utils/pagination'

// Type guard and normalization for feedbackType
function normalizeFeedbackTypeParam(val: unknown): ChatMessageRatingType[] {
// Supported as string or array (possibly from req.query)
const validValues = Object.values(ChatMessageRatingType);

Check failure on line 17 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
if (typeof val === 'string') {
// Could be CSV, single value, or JSON array string
try {
// Try JSON parse if it's an array-like
const parsed = JSON.parse(val);

Check failure on line 22 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
if (Array.isArray(parsed)) {
// Filter & map only valid rating types
return parsed.filter((x) => typeof x === 'string' && validValues.includes(x as ChatMessageRatingType));

Check failure on line 25 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
} else if (typeof parsed === 'string' && validValues.includes(parsed)) {
return [parsed];

Check failure on line 27 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
}
} catch {
// Not JSON, fall through
}
// CSV or single value
return val
.split(',')
.map((x) => x.trim())
.filter((x) => validValues.includes(x as ChatMessageRatingType)) as ChatMessageRatingType[];

Check failure on line 36 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
} else if (Array.isArray(val)) {
return val.filter((x) => typeof x === 'string' && validValues.includes(x as ChatMessageRatingType));

Check failure on line 38 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
}
return [];

Check failure on line 40 in packages/server/src/controllers/chat-messages/index.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

Delete `;`
}

const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => {
try {
let feedbackTypeFilters
Expand Down Expand Up @@ -75,9 +104,12 @@

const { page, limit } = getPageAndLimitParams(req)

let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (feedbackTypeFilters) {
// Always normalize and sanitize feedbackType param to ChatMessageRatingType[]
let feedbackTypeFilters = normalizeFeedbackTypeParam(req.query?.feedbackType)
if (feedbackTypeFilters && feedbackTypeFilters.length > 0) {
feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters)
} else {
feedbackTypeFilters = undefined
}
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
Expand Down
17 changes: 17 additions & 0 deletions packages/server/src/enterprise/sso/AzureSSO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ class AzureSSO extends SSOBase {
static async testSetup(ssoConfig: any) {
const { tenantID, clientID, clientSecret } = ssoConfig

// Validate tenantID before proceeding!
if (!AzureSSO.isValidTenantID(tenantID)) {
return { error: 'Invalid tenantID format.' }
}

try {
const tokenResponse = await axios.post(
`https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`,
Expand All @@ -136,6 +141,18 @@ class AzureSSO extends SSOBase {
}
}

/**
* Validate a Microsoft tenant ID (should be a UUID or domain ending with .onmicrosoft.com)
*/
static isValidTenantID(tenantID: string): boolean {
if (typeof tenantID !== 'string') return false;
// UUID v4 regex (Azure tenant ID is often a GUID), case insensitive
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// .onmicrosoft.com domain, loose check
const domainRegex = /^[a-zA-Z0-9-]+\.onmicrosoft\.com$/;
return uuidRegex.test(tenantID) || domainRegex.test(tenantID);
}

async refreshToken(ssoRefreshToken: string) {
const { tenantID, clientID, clientSecret } = this.ssoConfig

Expand Down
23 changes: 21 additions & 2 deletions packages/server/src/services/evaluations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,34 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str
})
;(dataset as any).rows = items

// Validate chatflowId - ensure all provided IDs exist in the database
let chatflowIds: string[] = [];
try {
chatflowIds = JSON.parse(body.chatflowId);
} catch (error) {
// fallback: treat as single id
chatflowIds = [body.chatflowId];
}
// Only allow strings that do not contain slashes
if (chatflowIds.some((id) => typeof id !== 'string' || id.includes('/') || id.includes('\\'))) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'Invalid chatflowId format');
}
const appServer = getRunningExpressApp();
const existingChatflows = await appServer.AppDataSource.getRepository(ChatFlow).findBy({
id: In(chatflowIds)
});
if (existingChatflows.length !== chatflowIds.length) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, 'One or more chatflowIds not found');
}
const data: ICommonObject = {
chatflowId: body.chatflowId,
dataset: dataset,
evaluationType: body.evaluationType,
evaluationId: newEvaluation.id,
credentialId: body.credentialId
}
};
if (body.datasetAsOneConversation) {
data.sessionId = uuidv4()
data.sessionId = uuidv4();
}

// When chatflow has an APIKey
Expand Down
Loading