From c71303b3ef112bed3e5567ad4372daf7988d9956 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 7 Nov 2025 14:47:48 +0530 Subject: [PATCH 01/25] feat: handle 429 errors and redirect to rate-limited page --- packages/ui/src/routes/AuthRoutes.jsx | 5 + .../ui/src/store/context/ErrorContext.jsx | 5 +- packages/ui/src/views/auth/rateLimited.jsx | 116 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/views/auth/rateLimited.jsx diff --git a/packages/ui/src/routes/AuthRoutes.jsx b/packages/ui/src/routes/AuthRoutes.jsx index 2d63fc38719..eb303b98c13 100644 --- a/packages/ui/src/routes/AuthRoutes.jsx +++ b/packages/ui/src/routes/AuthRoutes.jsx @@ -10,6 +10,7 @@ const VerifyEmailPage = Loadable(lazy(() => import('@/views/auth/verify-email')) const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword'))) const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword'))) const UnauthorizedPage = Loadable(lazy(() => import('@/views/auth/unauthorized'))) +const RateLimitedPage = Loadable(lazy(() => import('@/views/auth/rateLimited'))) const OrganizationSetupPage = Loadable(lazy(() => import('@/views/organization/index'))) const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired'))) @@ -45,6 +46,10 @@ const AuthRoutes = { path: '/unauthorized', element: }, + { + path: '/rate-limited', + element: + }, { path: '/organization-setup', element: diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index e41070a1516..4a4870d3f8f 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -14,7 +14,10 @@ export const ErrorProvider = ({ children }) => { const handleError = async (err) => { console.error(err) - if (err?.response?.status === 403) { + if (err?.response?.status === 429) { + const retryAfter = parseInt(err?.response?.headers?.['retry-after']) || 60 + navigate('/rate-limited', { state: { retryAfter } }) + } else if (err?.response?.status === 403) { navigate('/unauthorized') } else if (err?.response?.status === 401) { if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) { diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx new file mode 100644 index 00000000000..fdcb08cfadc --- /dev/null +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react' +import MainCard from '@/ui-component/cards/MainCard' +import { Box, Stack, Typography, LinearProgress } from '@mui/material' +import unauthorizedSVG from '@/assets/images/unauthorized.svg' +import { StyledButton } from '@/ui-component/button/StyledButton' +import { useNavigate, useLocation } from 'react-router-dom' +import { useSelector } from 'react-redux' + +// ==============================|| RateLimitedPage ||============================== // + +const RateLimitedPage = () => { + const navigate = useNavigate() + const location = useLocation() + const currentUser = useSelector((state) => state.auth.user) + + const retryAfter = location.state?.retryAfter || 60 + const [countdown, setCountdown] = useState(retryAfter) + const [canRetry, setCanRetry] = useState(false) + + useEffect(() => { + if (countdown <= 0) { + setCanRetry(true) + return + } + + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + setCanRetry(true) + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, [countdown]) + + const handleRetry = () => { + navigate(-1) + } + + const handleGoHome = () => { + navigate('/') + } + + const progress = ((retryAfter - countdown) / retryAfter) * 100 + + return ( + <> + + + + + rateLimitedSVG + + + 429 Too Many Requests + + + You've made too many requests in a short period of time. Please wait a moment before trying again. + + + {!canRetry && ( + + + Please wait {countdown} second{countdown !== 1 ? 's' : ''} + + + + )} + + + + Try Again + + {currentUser && ( + + Back to Home + + )} + + + + + + ) +} + +export default RateLimitedPage From bee8ce8c239c78a7a659d1c23d413861be799d27 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 7 Nov 2025 14:58:00 +0530 Subject: [PATCH 02/25] fix: simplify rate-limited page and better 429 error handling --- .../ui/src/store/context/ErrorContext.jsx | 14 +- packages/ui/src/views/auth/rateLimited.jsx | 128 ++++-------------- 2 files changed, 42 insertions(+), 100 deletions(-) diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index 4a4870d3f8f..9201242acf4 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -15,7 +15,19 @@ export const ErrorProvider = ({ children }) => { const handleError = async (err) => { console.error(err) if (err?.response?.status === 429) { - const retryAfter = parseInt(err?.response?.headers?.['retry-after']) || 60 + const retryAfterHeader = err?.response?.headers?.['retry-after'] + let retryAfter = 60 // Default in seconds + if (retryAfterHeader) { + const parsedSeconds = parseInt(retryAfterHeader, 10) + if (Number.isNaN(parsedSeconds)) { + const retryDate = new Date(retryAfterHeader) + if (!Number.isNaN(retryDate.getTime())) { + retryAfter = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000)) + } + } else { + retryAfter = parsedSeconds + } + } navigate('/rate-limited', { state: { retryAfter } }) } else if (err?.response?.status === 403) { navigate('/unauthorized') diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx index fdcb08cfadc..ac1093a11d3 100644 --- a/packages/ui/src/views/auth/rateLimited.jsx +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -1,115 +1,45 @@ -import { useEffect, useState } from 'react' -import MainCard from '@/ui-component/cards/MainCard' -import { Box, Stack, Typography, LinearProgress } from '@mui/material' +import { Box, Stack, Typography } from '@mui/material' +import { useLocation } from 'react-router-dom' import unauthorizedSVG from '@/assets/images/unauthorized.svg' -import { StyledButton } from '@/ui-component/button/StyledButton' -import { useNavigate, useLocation } from 'react-router-dom' -import { useSelector } from 'react-redux' +import MainCard from '@/ui-component/cards/MainCard' // ==============================|| RateLimitedPage ||============================== // const RateLimitedPage = () => { - const navigate = useNavigate() const location = useLocation() - const currentUser = useSelector((state) => state.auth.user) - - const retryAfter = location.state?.retryAfter || 60 - const [countdown, setCountdown] = useState(retryAfter) - const [canRetry, setCanRetry] = useState(false) - - useEffect(() => { - if (countdown <= 0) { - setCanRetry(true) - return - } - - const timer = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { - setCanRetry(true) - return 0 - } - return prev - 1 - }) - }, 1000) - - return () => clearInterval(timer) - }, [countdown]) - const handleRetry = () => { - navigate(-1) - } - - const handleGoHome = () => { - navigate('/') - } - - const progress = ((retryAfter - countdown) / retryAfter) * 100 + const retryAfter = location.state?.retryAfter || 60 return ( - <> - - + + - - - rateLimitedSVG - - - 429 Too Many Requests - - - You've made too many requests in a short period of time. Please wait a moment before trying again. - - - {!canRetry && ( - - - Please wait {countdown} second{countdown !== 1 ? 's' : ''} - - - - )} - - - - Try Again - - {currentUser && ( - - Back to Home - - )} - - - - - + + rateLimitedSVG + + + 429 Too Many Requests + + + {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`} + + + + ) } From 2a08897107c274434f21099a70689440390f1e1c Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 10 Nov 2025 13:21:42 +0530 Subject: [PATCH 03/25] fix: status code in quotaUsage --- packages/server/src/utils/quotaUsage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/quotaUsage.ts b/packages/server/src/utils/quotaUsage.ts index e2cf382d4dc..35cf855aa1b 100644 --- a/packages/server/src/utils/quotaUsage.ts +++ b/packages/server/src/utils/quotaUsage.ts @@ -70,7 +70,7 @@ export const checkUsageLimit = async ( if (limit === -1) return if (currentUsage > limit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, `Limit exceeded: ${type}`) + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, `Limit exceeded: ${type}`) } } @@ -135,7 +135,7 @@ export const checkPredictions = async (orgId: string, subscriptionId: string, us if (predictionsLimit === -1) return if (currentPredictions >= predictionsLimit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Predictions limit exceeded') + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Predictions limit exceeded') } return { @@ -161,7 +161,7 @@ export const checkStorage = async (orgId: string, subscriptionId: string, usageC if (storageLimit === -1) return if (currentStorageUsage >= storageLimit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Storage limit exceeded') + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Storage limit exceeded') } return { From f6b566c68fe09af0d8d54c0fddcaf6f0832eeb48 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 10 Nov 2025 14:50:53 +0530 Subject: [PATCH 04/25] update: add back to home button rate-limited page --- packages/ui/src/views/auth/rateLimited.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx index ac1093a11d3..44b8a85dd6f 100644 --- a/packages/ui/src/views/auth/rateLimited.jsx +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -1,5 +1,5 @@ -import { Box, Stack, Typography } from '@mui/material' -import { useLocation } from 'react-router-dom' +import { Box, Button, Stack, Typography } from '@mui/material' +import { Link, useLocation } from 'react-router-dom' import unauthorizedSVG from '@/assets/images/unauthorized.svg' import MainCard from '@/ui-component/cards/MainCard' @@ -37,6 +37,11 @@ const RateLimitedPage = () => { {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`} + + + From aefe3ada626563001a2c44bfd66c96b5f98b4b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:29:14 +0700 Subject: [PATCH 05/25] chore: fix typos in docker/worker/Dockerfile (#5435) Fix typos in docker/worker/Dockerfile --- docker/worker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile index 655b3de4785..8a2c749d44e 100644 --- a/docker/worker/Dockerfile +++ b/docker/worker/Dockerfile @@ -7,7 +7,7 @@ RUN apk add --no-cache build-base cairo-dev pango-dev # Install Chromium and curl for container-level health checks RUN apk add --no-cache chromium curl -#install PNPM globaly +#install PNPM globally RUN npm install -g pnpm ENV PUPPETEER_SKIP_DOWNLOAD=true From 4df444d5c6a108b581bdd881cbabeb816913237c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:29:29 +0700 Subject: [PATCH 06/25] chore: fix typos in packages/components/nodes/agentflow/Condition/Condition.ts (#5436) Fix typos in packages/components/nodes/agentflow/Condition/Condition.ts --- packages/components/nodes/agentflow/Condition/Condition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/agentflow/Condition/Condition.ts b/packages/components/nodes/agentflow/Condition/Condition.ts index 6913aac1947..7ae1be06291 100644 --- a/packages/components/nodes/agentflow/Condition/Condition.ts +++ b/packages/components/nodes/agentflow/Condition/Condition.ts @@ -317,7 +317,7 @@ class Condition_Agentflow implements INode { } } - // If no condition is fullfilled, add isFulfilled to the ELSE condition + // If no condition is fulfilled, add isFulfilled to the ELSE condition const dummyElseConditionData = { type: 'string', value1: '', From 8b66ac7d9dc06c2aab58a24810b9c3cbd3748b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:29:44 +0700 Subject: [PATCH 07/25] chore: fix typos in packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts (#5437) Fix typos in packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts --- .../nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts index 29d1b74e584..5e238410219 100644 --- a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts +++ b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts @@ -103,7 +103,7 @@ class ChatHuggingFace_ChatModels implements INode { type: 'string', rows: 4, placeholder: 'AI assistant:', - description: 'Sets the stop sequences to use. Use comma to seperate different sequences.', + description: 'Sets the stop sequences to use. Use comma to separate different sequences.', optional: true, additionalParams: true } From 51755b1d1a044a51486eff066fee688f81835205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:30:01 +0700 Subject: [PATCH 08/25] chore: fix typos in packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts (#5438) Fix typos in packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts --- .../nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts index b1d1f6dd3c3..9e562962a4a 100644 --- a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts +++ b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts @@ -11,7 +11,7 @@ return [ tool_calls: [ { id: "12345", - name: "calulator", + name: "calculator", args: { number1: 333382, number2: 1932, From e741898184cb4257a77c9a5fe6f24db1d0964f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:48:15 +0700 Subject: [PATCH 09/25] docs: fix typos in packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx (#5444) Fix typos in packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx --- .../src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx index f965e0e72f4..054f409c9e7 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx @@ -58,7 +58,7 @@ const NavGroup = ({ item }) => { const renderNonPrimaryGroups = () => { let nonprimaryGroups = item.children.filter((child) => child.id !== 'primary') - // Display chilren based on permission and display + // Display children based on permission and display nonprimaryGroups = nonprimaryGroups.map((group) => { const children = group.children.filter((menu) => shouldDisplayMenu(menu)) return { ...group, children } From 75669997296c6c22a7a558777371e45b98e1a402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:48:31 +0700 Subject: [PATCH 10/25] docs: fix typos in packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts (#5446) Fix typos in packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts --- .../engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts index b19eb2346ae..02862c74045 100644 --- a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts +++ b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts @@ -39,7 +39,7 @@ class SubQuestionQueryEngine_LlamaIndex implements INode { this.icon = 'subQueryEngine.svg' this.category = 'Engine' this.description = - 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response' + 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate responses and synthesizes a final response' this.baseClasses = [this.type, 'BaseQueryEngine'] this.tags = ['LlamaIndex'] this.inputs = [ From 12131b362b3158ab15ee49800ba02a81d44de28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:48:47 +0700 Subject: [PATCH 11/25] docs: fix typos in packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts (#5447) Fix typos in packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts --- .../nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index 4946fa8bbc2..5ee12705e38 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -96,7 +96,7 @@ class AWSBedrockEmbedding_Embeddings implements INode { { label: 'Max AWS API retries', name: 'maxRetries', - description: 'This will limit the nubmer of AWS API for Titan model embeddings call retries. Used to avoid throttling.', + description: 'This will limit the number of AWS API for Titan model embeddings call retries. Used to avoid throttling.', type: 'number', optional: true, default: 5, From 656f68fe1ffea44a57093fd8e8cb3c87d2c8a6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:49:06 +0700 Subject: [PATCH 12/25] docs: fix typos in packages/server/README.md (#5445) Fix typos in packages/server/README.md --- packages/server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/README.md b/packages/server/README.md index af29ebea246..c3c6c9a16d6 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -41,7 +41,7 @@ cd Flowise/packages/server pnpm install ./node_modules/.bin/cypress install pnpm build -#Only for writting new tests on local dev -> pnpm run cypress:open +#Only for writing new tests on local dev -> pnpm run cypress:open pnpm run e2e ``` From 901eca113117b81301c1ace011ec1db15e2910fa Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 7 Nov 2025 11:51:54 +0000 Subject: [PATCH 13/25] Bugfix/Supervisor Node AzureChatOpenAI (#5448) Integrate AzureChatOpenAI into the Supervisor node to handle user requests alongside ChatOpenAI. This enhancement allows for improved multi-agent conversation management. --- packages/components/nodes/multiagents/Supervisor/Supervisor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/multiagents/Supervisor/Supervisor.ts b/packages/components/nodes/multiagents/Supervisor/Supervisor.ts index f67abf00b19..2babee9aa02 100644 --- a/packages/components/nodes/multiagents/Supervisor/Supervisor.ts +++ b/packages/components/nodes/multiagents/Supervisor/Supervisor.ts @@ -21,6 +21,7 @@ import { ChatOpenAI } from '../../chatmodels/ChatOpenAI/FlowiseChatOpenAI' import { ChatAnthropic } from '../../chatmodels/ChatAnthropic/FlowiseChatAnthropic' import { addImagesToMessages, llmSupportsVision } from '../../../src/multiModalUtils' import { ChatGoogleGenerativeAI } from '../../chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI' +import { AzureChatOpenAI } from '../../chatmodels/AzureChatOpenAI/FlowiseAzureChatOpenAI' const sysPrompt = `You are a supervisor tasked with managing a conversation between the following workers: {team_members}. Given the following user request, respond with the worker to act next. @@ -242,7 +243,7 @@ class Supervisor_MultiAgents implements INode { } } }) - } else if (llm instanceof ChatOpenAI) { + } else if (llm instanceof ChatOpenAI || llm instanceof AzureChatOpenAI) { let prompt = ChatPromptTemplate.fromMessages([ ['system', systemPrompt], new MessagesPlaceholder('messages'), From f0a24518c7329c57efa4545fbeab9e0825b377e9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 13 Nov 2025 11:11:39 +0000 Subject: [PATCH 14/25] Chore/JSON Array (#5467) * add separate by JSON object * add file check for Unstructured * Enhance JSON DocumentLoader: Update label and description for 'Separate by JSON Object' option, and add type check for JSON objects in array processing. --- .../nodes/documentloaders/Json/Json.ts | 81 +++++++++++++++---- .../Unstructured/UnstructuredFile.ts | 4 +- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/packages/components/nodes/documentloaders/Json/Json.ts b/packages/components/nodes/documentloaders/Json/Json.ts index f94138a4c0c..042c81ef833 100644 --- a/packages/components/nodes/documentloaders/Json/Json.ts +++ b/packages/components/nodes/documentloaders/Json/Json.ts @@ -47,7 +47,7 @@ class Json_DocumentLoaders implements INode { constructor() { this.label = 'Json File' this.name = 'jsonFile' - this.version = 3.0 + this.version = 3.1 this.type = 'Document' this.icon = 'json.svg' this.category = 'Document Loaders' @@ -66,6 +66,14 @@ class Json_DocumentLoaders implements INode { type: 'TextSplitter', optional: true }, + { + label: 'Separate by JSON Object (JSON Array)', + name: 'separateByObject', + type: 'boolean', + description: 'If enabled and the file is a JSON Array, each JSON object will be extracted as a chunk', + optional: true, + additionalParams: true + }, { label: 'Pointers Extraction (separated by commas)', name: 'pointersName', @@ -73,7 +81,10 @@ class Json_DocumentLoaders implements INode { description: 'Ex: { "key": "value" }, Pointer Extraction = "key", "value" will be extracted as pageContent of the chunk. Use comma to separate multiple pointers', placeholder: 'key1, key2', - optional: true + optional: true, + hide: { + separateByObject: true + } }, { label: 'Additional Metadata', @@ -122,6 +133,7 @@ class Json_DocumentLoaders implements INode { const pointersName = nodeData.inputs?.pointersName as string const metadata = nodeData.inputs?.metadata const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + const separateByObject = nodeData.inputs?.separateByObject as boolean const output = nodeData.outputs?.output as string let omitMetadataKeys: string[] = [] @@ -153,7 +165,7 @@ class Json_DocumentLoaders implements INode { if (!file) continue const fileData = await getFileFromStorage(file, orgId, chatflowid) const blob = new Blob([fileData]) - const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata) + const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject) if (textSplitter) { let splittedDocs = await loader.load() @@ -176,7 +188,7 @@ class Json_DocumentLoaders implements INode { splitDataURI.pop() const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const blob = new Blob([bf]) - const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata) + const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject) if (textSplitter) { let splittedDocs = await loader.load() @@ -306,13 +318,20 @@ class TextLoader extends BaseDocumentLoader { class JSONLoader extends TextLoader { public pointers: string[] private metadataMapping: Record - - constructor(filePathOrBlob: string | Blob, pointers: string | string[] = [], metadataMapping: Record = {}) { + private separateByObject: boolean + + constructor( + filePathOrBlob: string | Blob, + pointers: string | string[] = [], + metadataMapping: Record = {}, + separateByObject: boolean = false + ) { super(filePathOrBlob) this.pointers = Array.isArray(pointers) ? pointers : [pointers] if (metadataMapping) { this.metadataMapping = typeof metadataMapping === 'object' ? metadataMapping : JSON.parse(metadataMapping) } + this.separateByObject = separateByObject } protected async parse(raw: string): Promise { @@ -323,14 +342,24 @@ class JSONLoader extends TextLoader { const jsonArray = Array.isArray(json) ? json : [json] for (const item of jsonArray) { - const content = this.extractContent(item) - const metadata = this.extractMetadata(item) - - for (const pageContent of content) { - documents.push({ - pageContent, - metadata - }) + if (this.separateByObject) { + if (typeof item === 'object' && item !== null && !Array.isArray(item)) { + const metadata = this.extractMetadata(item) + const pageContent = this.formatObjectAsKeyValue(item) + documents.push({ + pageContent, + metadata + }) + } + } else { + const content = this.extractContent(item) + const metadata = this.extractMetadata(item) + for (const pageContent of content) { + documents.push({ + pageContent, + metadata + }) + } } } @@ -370,6 +399,30 @@ class JSONLoader extends TextLoader { return metadata } + /** + * Formats a JSON object as readable key-value pairs + */ + private formatObjectAsKeyValue(obj: any, prefix: string = ''): string { + const lines: string[] = [] + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + lines.push(`${fullKey}: ${value}`) + } else if (Array.isArray(value)) { + lines.push(`${fullKey}: ${JSON.stringify(value)}`) + } else if (typeof value === 'object') { + // Recursively format nested objects + lines.push(this.formatObjectAsKeyValue(value, fullKey)) + } else { + lines.push(`${fullKey}: ${value}`) + } + } + + return lines.join('\n') + } + /** * If JSON pointers are specified, return all strings below any of them * and exclude all other nodes expect if they match a JSON pointer. diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts index 808b7ef0d76..e1842e27cdb 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts @@ -10,7 +10,7 @@ import { import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' import { getFileFromStorage, INodeOutputsValue } from '../../../src' import { UnstructuredLoader } from './Unstructured' -import { isPathTraversal } from '../../../src/validator' +import { isPathTraversal, isUnsafeFilePath } from '../../../src/validator' import sanitize from 'sanitize-filename' import path from 'path' @@ -565,7 +565,7 @@ class UnstructuredFile_DocumentLoaders implements INode { throw new Error('Invalid file path format') } - if (isPathTraversal(filePath)) { + if (isPathTraversal(filePath) || isUnsafeFilePath(filePath)) { throw new Error('Invalid path characters detected in filePath - path traversal not allowed') } From 35b5d0c906da29d6c0332427e16f418354856834 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 11:16:42 +0000 Subject: [PATCH 15/25] Chore/Remove Deprecated File Path Unstructured (#5478) * Refactor UnstructuredFile and UnstructuredFolder loaders to remove deprecated file path handling and enhance folder path validation. Ensure folder paths are sanitized and validated against path traversal attacks. * Update UnstructuredFolder.ts --- .../Unstructured/UnstructuredFile.ts | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts index e1842e27cdb..d1a372b0c94 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts @@ -4,15 +4,11 @@ import { UnstructuredLoaderOptions, UnstructuredLoaderStrategy, SkipInferTableTypes, - HiResModelName, - UnstructuredLoader as LCUnstructuredLoader + HiResModelName } from '@langchain/community/document_loaders/fs/unstructured' import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' import { getFileFromStorage, INodeOutputsValue } from '../../../src' import { UnstructuredLoader } from './Unstructured' -import { isPathTraversal, isUnsafeFilePath } from '../../../src/validator' -import sanitize from 'sanitize-filename' -import path from 'path' class UnstructuredFile_DocumentLoaders implements INode { label: string @@ -44,17 +40,6 @@ class UnstructuredFile_DocumentLoaders implements INode { optional: true } this.inputs = [ - /** Deprecated - { - label: 'File Path', - name: 'filePath', - type: 'string', - placeholder: '', - optional: true, - warning: - 'Use the File Upload instead of File path. If file is uploaded, this path is ignored. Path will be deprecated in future releases.' - }, - */ { label: 'Files Upload', name: 'fileObject', @@ -455,7 +440,6 @@ class UnstructuredFile_DocumentLoaders implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const filePath = nodeData.inputs?.filePath as string const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy const encoding = nodeData.inputs?.encoding as string @@ -560,37 +544,8 @@ class UnstructuredFile_DocumentLoaders implements INode { docs.push(...loaderDocs) } } - } else if (filePath) { - if (!filePath || typeof filePath !== 'string') { - throw new Error('Invalid file path format') - } - - if (isPathTraversal(filePath) || isUnsafeFilePath(filePath)) { - throw new Error('Invalid path characters detected in filePath - path traversal not allowed') - } - - const parsedPath = path.parse(filePath) - const sanitizedFilename = sanitize(parsedPath.base) - - if (!sanitizedFilename || sanitizedFilename.trim() === '') { - throw new Error('Invalid filename after sanitization') - } - - const sanitizedFilePath = path.join(parsedPath.dir, sanitizedFilename) - - if (!path.isAbsolute(sanitizedFilePath)) { - throw new Error('File path must be absolute') - } - - if (sanitizedFilePath.includes('..')) { - throw new Error('Invalid file path - directory traversal not allowed') - } - - const loader = new LCUnstructuredLoader(sanitizedFilePath, obj) - const loaderDocs = await loader.load() - docs.push(...loaderDocs) } else { - throw new Error('File path or File upload is required') + throw new Error('File upload is required') } if (metadata) { From c6a162cbdfe820e27cf6c83ec9bdf82f627a617c Mon Sep 17 00:00:00 2001 From: Taraka Vishnumolakala Date: Sat, 15 Nov 2025 10:03:01 -0500 Subject: [PATCH 16/25] =?UTF-8?q?feat(security):=20enhance=20file=20path?= =?UTF-8?q?=20validation=20and=20implement=20non-root=20D=E2=80=A6=20(#547?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(security): enhance file path validation and implement non-root Docker user - Validate resolved full file paths including workspace boundaries in SecureFileStore - Resolve paths before validation in readFile and writeFile operations - Run Docker container as non-root flowise user (uid/gid 1001) - Apply proper file ownership and permissions for application files Prevents path traversal attacks and follows container security best practices * Add sensitive system directory validation and Flowise internal file protection * Update Dockerfile to use default node user * update validation patterns to include additional system binary directories (/usr/bin, /usr/sbin, /usr/local/bin) * added isSafeBrowserExecutable function to validate browser executable paths for Playwright and Puppeteer loaders --------- Co-authored-by: taraka-vishnumolakala Co-authored-by: Henry Heng Co-authored-by: Henry --- Dockerfile | 36 +++--- .../documentloaders/Playwright/Playwright.ts | 9 +- .../documentloaders/Puppeteer/Puppeteer.ts | 9 +- packages/components/src/SecureFileStore.ts | 49 ++++++-- packages/components/src/validator.ts | 113 ++++++++++++++++++ 5 files changed, 188 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index a824b7f8090..d03004de737 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,34 +5,38 @@ # docker run -d -p 3000:3000 flowise FROM node:20-alpine -RUN apk add --update libc6-compat python3 make g++ -# needed for pdfjs-dist -RUN apk add --no-cache build-base cairo-dev pango-dev -# Install Chromium -RUN apk add --no-cache chromium - -# Install curl for container-level health checks -# Fixes: https://github.com/FlowiseAI/Flowise/issues/4126 -RUN apk add --no-cache curl - -#install PNPM globaly -RUN npm install -g pnpm +# Install system dependencies and build tools +RUN apk update && \ + apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ \ + build-base \ + cairo-dev \ + pango-dev \ + chromium \ + curl && \ + npm install -g pnpm ENV PUPPETEER_SKIP_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser ENV NODE_OPTIONS=--max-old-space-size=8192 -WORKDIR /usr/src +WORKDIR /usr/src/flowise # Copy app source COPY . . -RUN pnpm install +# Install dependencies and build +RUN pnpm install && \ + pnpm build -RUN pnpm build +# Switch to non-root user (node user already exists in node:20-alpine) +USER node EXPOSE 3000 -CMD [ "pnpm", "start" ] +CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index c3b090e8b10..8a40d7ea208 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -10,6 +10,7 @@ import { test } from 'linkifyjs' import { omit } from 'lodash' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { isSafeBrowserExecutable } from '../../../src/validator' class Playwright_DocumentLoaders implements INode { label: string @@ -190,11 +191,17 @@ class Playwright_DocumentLoaders implements INode { async function playwrightLoader(url: string): Promise { try { let docs = [] + + const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH + if (!isSafeBrowserExecutable(executablePath)) { + throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) + } + const config: PlaywrightWebBaseLoaderOptions = { launchOptions: { args: ['--no-sandbox'], headless: true, - executablePath: process.env.PLAYWRIGHT_EXECUTABLE_FILE_PATH + executablePath: executablePath } } if (waitUntilGoToOption) { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 5409ef4f02b..0e5bdacb8a8 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -6,6 +6,7 @@ import { omit } from 'lodash' import { PuppeteerLifeCycleEvent } from 'puppeteer' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { isSafeBrowserExecutable } from '../../../src/validator' class Puppeteer_DocumentLoaders implements INode { label: string @@ -181,11 +182,17 @@ class Puppeteer_DocumentLoaders implements INode { async function puppeteerLoader(url: string): Promise { try { let docs: Document[] = [] + + const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH + if (!isSafeBrowserExecutable(executablePath)) { + throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) + } + const config: PuppeteerWebBaseLoaderOptions = { launchOptions: { args: ['--no-sandbox'], headless: 'new', - executablePath: process.env.PUPPETEER_EXECUTABLE_FILE_PATH + executablePath: executablePath } } if (waitUntilGoToOption) { diff --git a/packages/components/src/SecureFileStore.ts b/packages/components/src/SecureFileStore.ts index 88981ecbff2..fc50d7732f3 100644 --- a/packages/components/src/SecureFileStore.ts +++ b/packages/components/src/SecureFileStore.ts @@ -1,8 +1,8 @@ import { Serializable } from '@langchain/core/load/serializable' +import * as fs from 'fs' import { NodeFileStore } from 'langchain/stores/file/node' -import { isUnsafeFilePath, isWithinWorkspace } from './validator' import * as path from 'path' -import * as fs from 'fs' +import { isSensitiveSystemPath, isUnsafeFilePath, isWithinWorkspace } from './validator' /** * Security configuration for file operations @@ -65,28 +65,50 @@ export class SecureFileStore extends Serializable { throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`) } + // Validate that workspace path is not a sensitive system directory + // This prevents setting workspace to /usr/bin, /etc, etc. which would allow access to system files + if (isSensitiveSystemPath(path.normalize(this.config.workspacePath))) { + throw new Error(`Workspace path cannot be set to sensitive system directory: ${this.config.workspacePath}`) + } + // Initialize the underlying NodeFileStore with workspace path this.nodeFileStore = new NodeFileStore(this.config.workspacePath) } /** * Validates a file path against security policies + * @param filePath The raw user-provided file path (relative to workspace) + * @param resolvedPath The resolved absolute path (for extension validation) */ - private validateFilePath(filePath: string): void { - // Check for unsafe path patterns + private validateFilePath(filePath: string, resolvedPath: string): void { + // Validate the raw user input for unsafe patterns (path traversal, absolute paths, etc.) + // This must be done on the raw input, not the resolved path, because isUnsafeFilePath + // is designed to detect absolute paths in user input if (isUnsafeFilePath(filePath)) { throw new Error(`Unsafe file path detected: ${filePath}`) } - // Enforce workspace boundaries if enabled + // Enforce workspace boundaries if enabled (this handles path resolution internally) if (this.config.enforceWorkspaceBoundaries) { if (!isWithinWorkspace(filePath, this.config.workspacePath)) { throw new Error(`File path outside workspace boundaries: ${filePath}`) } } - // Check file extension - const ext = path.extname(filePath).toLowerCase() + // Prevent access to Flowise internal files (any path containing .flowise) + const normalizedResolved = path.normalize(resolvedPath) + if (normalizedResolved.includes('.flowise')) { + throw new Error(`Access to Flowise internal files denied: ${filePath}`) + } + + // Validate that the resolved path does not access sensitive system directories + // This prevents access to system files even if workspace is set to a system directory + if (isSensitiveSystemPath(normalizedResolved)) { + throw new Error(`Access to sensitive system directory denied: ${filePath}`) + } + + // Check file extension on the resolved path to get the actual extension + const ext = path.extname(resolvedPath).toLowerCase() // Check blocked extensions if (this.config.blockedExtensions.includes(ext)) { @@ -113,7 +135,10 @@ export class SecureFileStore extends Serializable { * Reads a file with security validation */ async readFile(filePath: string): Promise { - this.validateFilePath(filePath) + // Resolve the full path for extension validation + const resolvedPath = path.resolve(this.config.workspacePath, filePath) + // Validate the raw user input (not the resolved path) to avoid false positives + this.validateFilePath(filePath, resolvedPath) try { return await this.nodeFileStore.readFile(filePath) @@ -127,12 +152,16 @@ export class SecureFileStore extends Serializable { * Writes a file with security validation */ async writeFile(filePath: string, contents: string): Promise { - this.validateFilePath(filePath) this.validateFileSize(contents) + // Resolve the full path for extension validation and directory creation + const resolvedPath = path.resolve(this.config.workspacePath, filePath) + // Validate the raw user input (not the resolved path) to avoid false positives + this.validateFilePath(filePath, resolvedPath) + try { // Ensure the directory exists - const dir = path.dirname(path.resolve(this.config.workspacePath, filePath)) + const dir = path.dirname(resolvedPath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index 26cad65d935..f185f181187 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -70,6 +70,35 @@ export const isUnsafeFilePath = (filePath: string): boolean => { return dangerousPatterns.some((pattern) => pattern.test(filePath)) } +/** + * Validates if a resolved path accesses sensitive system directories + * Uses pattern-based detection to identify known sensitive system directories + * at root level or one level deep, while allowing legitimate paths like /usr/src + * @param {string} resolvedPath The resolved absolute path to validate + * @returns {boolean} True if path accesses sensitive system directory, false otherwise + */ +export const isSensitiveSystemPath = (resolvedPath: string): boolean => { + if (!resolvedPath || typeof resolvedPath !== 'string') { + return false + } + + // Pattern-based detection for known sensitive system directories: + // Blocks obvious system directories while allowing legitimate paths like /usr/src, /usr/local/src, /opt, etc. + // 1. At root level (e.g., /etc, /sys, /bin, /sbin) - one segment after root + // 2. One level deep (e.g., /etc/passwd, /sys/kernel, /var/log) - two segments total + // 3. Specific sensitive subdirectories (e.g., /var/log, /var/run) - two segments with specific parent + // 4. System binary directories (e.g., /usr/bin, /usr/sbin, /usr/local/bin) - prevents overwriting system executables + const sensitiveSystemPatterns = [ + /^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)([/\\]|$)/i, // Root level: /etc, /sys, /proc, /bin, /sbin, etc. + /^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)[/\\][^/\\]*$/i, // One level deep: /etc/passwd, /sys/kernel, /bin/sh, etc. + /^[/\\]var[/\\](log|run|lib|spool|mail)([/\\]|$)/i, // Sensitive /var subdirectories: /var/log, /var/run, etc. + /^[/\\]usr[/\\](bin|sbin)([/\\]|$)/i, // System binary directories: /usr/bin, /usr/sbin + /^[/\\]usr[/\\]local[/\\](bin|sbin)([/\\]|$)/i // Local system binaries: /usr/local/bin, /usr/local/sbin + ] + + return sensitiveSystemPatterns.some((pattern) => pattern.test(resolvedPath)) +} + /** * Validates if a file path is within the allowed workspace boundaries * @param {string} filePath The file path to validate @@ -102,3 +131,87 @@ export const isWithinWorkspace = (filePath: string, workspacePath: string): bool return false } } + +/** + * Validates if a browser executable path is safe to use + * Prevents arbitrary code execution through environment variable manipulation + * @param {string} executablePath The browser executable path to validate + * @returns {boolean} True if path is safe, false otherwise + */ +export const isSafeBrowserExecutable = (executablePath: string | undefined): boolean => { + if (!executablePath) { + return true // If not specified, let browser library use its default + } + + if (typeof executablePath !== 'string' || executablePath.trim() === '') { + return false + } + + const path = require('path') + const fs = require('fs') + + try { + // Normalize the path + const normalizedPath = path.normalize(executablePath) + + // Must be an absolute path + if (!path.isAbsolute(normalizedPath)) { + return false + } + + // Allowed browser executable locations (system-managed only) + const allowedPaths = [ + // Linux/Unix Chromium/Chrome paths + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chrome', + '/snap/bin/chromium', + // macOS Chrome/Chromium paths + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + // Windows Chrome/Chromium paths (normalized with forward slashes) + 'C:/Program Files/Google/Chrome/Application/chrome.exe', + 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', + 'C:/Program Files/Chromium/Application/chrome.exe', + // Firefox paths + '/usr/bin/firefox', + '/Applications/Firefox.app/Contents/MacOS/firefox', + 'C:/Program Files/Mozilla Firefox/firefox.exe', + 'C:/Program Files (x86)/Mozilla Firefox/firefox.exe' + ] + + // Normalize allowed paths for comparison (handle Windows backslashes) + const normalizedAllowedPaths = allowedPaths.map((p) => path.normalize(p)) + + // Check if the path exactly matches one of the allowed paths + const isAllowedPath = normalizedAllowedPaths.some((allowedPath) => normalizedPath.toLowerCase() === allowedPath.toLowerCase()) + + if (!isAllowedPath) { + return false + } + + // Additional security: Verify file exists and is executable (where applicable) + // This prevents using a path before malicious file is written + try { + if (fs.existsSync(normalizedPath)) { + const stats = fs.statSync(normalizedPath) + // On Unix-like systems, check if file is executable + if (process.platform !== 'win32') { + // Check if file has execute permissions (using bitwise AND) + // 0o111 checks for execute permission for user, group, or others + return (stats.mode & 0o111) !== 0 + } + return stats.isFile() + } + // If file doesn't exist, reject it (prevents race conditions) + return false + } catch { + return false + } + } catch (error) { + // If any error occurs during validation, deny access + return false + } +} From 4e2f87d3a0f06504b4eb3eee39dc062adf1f980e Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 18:09:31 +0000 Subject: [PATCH 17/25] Chore/docker file non root (#5479) * update dockerfile * Update Dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index d03004de737..70041f41d47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,9 @@ COPY . . RUN pnpm install && \ pnpm build +# Give the node user ownership of the application files +RUN chown -R node:node . + # Switch to non-root user (node user already exists in node:20-alpine) USER node From cc84b9b41749e632e52a9af19e8c886080db7958 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 20:24:42 +0000 Subject: [PATCH 18/25] remove read write file tools and imports (#5480) --- .../documentloaders/Playwright/Playwright.ts | 4 - .../documentloaders/Puppeteer/Puppeteer.ts | 4 - .../nodes/tools/ReadFile/ReadFile.ts | 147 ------------- .../nodes/tools/ReadFile/readfile.svg | 4 - .../nodes/tools/WriteFile/WriteFile.ts | 149 ------------- .../nodes/tools/WriteFile/writefile.svg | 4 - packages/components/src/SecureFileStore.ts | 196 ------------------ packages/components/src/validator.ts | 146 ------------- 8 files changed, 654 deletions(-) delete mode 100644 packages/components/nodes/tools/ReadFile/ReadFile.ts delete mode 100644 packages/components/nodes/tools/ReadFile/readfile.svg delete mode 100644 packages/components/nodes/tools/WriteFile/WriteFile.ts delete mode 100644 packages/components/nodes/tools/WriteFile/writefile.svg delete mode 100644 packages/components/src/SecureFileStore.ts diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index 8a40d7ea208..f0507459347 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -10,7 +10,6 @@ import { test } from 'linkifyjs' import { omit } from 'lodash' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { isSafeBrowserExecutable } from '../../../src/validator' class Playwright_DocumentLoaders implements INode { label: string @@ -193,9 +192,6 @@ class Playwright_DocumentLoaders implements INode { let docs = [] const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH - if (!isSafeBrowserExecutable(executablePath)) { - throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) - } const config: PlaywrightWebBaseLoaderOptions = { launchOptions: { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 0e5bdacb8a8..9b4ada91661 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -6,7 +6,6 @@ import { omit } from 'lodash' import { PuppeteerLifeCycleEvent } from 'puppeteer' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { isSafeBrowserExecutable } from '../../../src/validator' class Puppeteer_DocumentLoaders implements INode { label: string @@ -184,9 +183,6 @@ class Puppeteer_DocumentLoaders implements INode { let docs: Document[] = [] const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH - if (!isSafeBrowserExecutable(executablePath)) { - throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) - } const config: PuppeteerWebBaseLoaderOptions = { launchOptions: { diff --git a/packages/components/nodes/tools/ReadFile/ReadFile.ts b/packages/components/nodes/tools/ReadFile/ReadFile.ts deleted file mode 100644 index eb703a1de90..00000000000 --- a/packages/components/nodes/tools/ReadFile/ReadFile.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class ReadFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Read File' - this.name = 'readFile' - this.version = 2.0 - this.type = 'ReadFile' - this.icon = 'readfile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.' - this.description = 'Read file from disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be read', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new ReadFileTool({ store }) - } -} - -interface ReadFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for reading files from the disk. Extends the StructuredTool - * class. - */ -export class ReadFileTool extends StructuredTool { - static lc_name() { - return 'ReadFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file') - }) as any - - name = 'read_file' - - description = 'Read file from disk' - - store: BaseFileStore - - constructor({ store }: ReadFileParams) { - super(...arguments) - - this.store = store - } - - async _call({ file_path }: z.infer) { - return await this.store.readFile(file_path) - } -} - -module.exports = { nodeClass: ReadFile_Tools } diff --git a/packages/components/nodes/tools/ReadFile/readfile.svg b/packages/components/nodes/tools/ReadFile/readfile.svg deleted file mode 100644 index c7cba0efa25..00000000000 --- a/packages/components/nodes/tools/ReadFile/readfile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/nodes/tools/WriteFile/WriteFile.ts b/packages/components/nodes/tools/WriteFile/WriteFile.ts deleted file mode 100644 index bc3609bebb5..00000000000 --- a/packages/components/nodes/tools/WriteFile/WriteFile.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class WriteFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Write File' - this.name = 'writeFile' - this.version = 2.0 - this.type = 'WriteFile' - this.icon = 'writefile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.' - this.description = 'Write file to disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be written', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new WriteFileTool({ store }) - } -} - -interface WriteFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for writing data to files on the disk. Extends the StructuredTool - * class. - */ -export class WriteFileTool extends StructuredTool { - static lc_name() { - return 'WriteFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file'), - text: z.string().describe('text to write to file') - }) as any - - name = 'write_file' - - description = 'Write file to disk' - - store: BaseFileStore - - constructor({ store, ...rest }: WriteFileParams) { - super(rest) - - this.store = store - } - - async _call({ file_path, text }: z.infer) { - await this.store.writeFile(file_path, text) - return `File written to ${file_path} successfully.` - } -} - -module.exports = { nodeClass: WriteFile_Tools } diff --git a/packages/components/nodes/tools/WriteFile/writefile.svg b/packages/components/nodes/tools/WriteFile/writefile.svg deleted file mode 100644 index 0df04ea44c3..00000000000 --- a/packages/components/nodes/tools/WriteFile/writefile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/src/SecureFileStore.ts b/packages/components/src/SecureFileStore.ts deleted file mode 100644 index fc50d7732f3..00000000000 --- a/packages/components/src/SecureFileStore.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Serializable } from '@langchain/core/load/serializable' -import * as fs from 'fs' -import { NodeFileStore } from 'langchain/stores/file/node' -import * as path from 'path' -import { isSensitiveSystemPath, isUnsafeFilePath, isWithinWorkspace } from './validator' - -/** - * Security configuration for file operations - */ -export interface FileSecurityConfig { - /** Base workspace path - all file operations are restricted to this directory */ - workspacePath: string - /** Whether to enforce workspace boundaries (default: true) */ - enforceWorkspaceBoundaries?: boolean - /** Maximum file size in bytes (default: 10MB) */ - maxFileSize?: number - /** Allowed file extensions (if empty, all extensions allowed) */ - allowedExtensions?: string[] - /** Blocked file extensions */ - blockedExtensions?: string[] -} - -/** - * Secure file store that enforces workspace boundaries and validates file operations - */ -export class SecureFileStore extends Serializable { - lc_namespace = ['flowise', 'components', 'stores', 'file'] - - private config: Required - private nodeFileStore: NodeFileStore - - constructor(config: FileSecurityConfig) { - super() - - // Set default configuration - this.config = { - workspacePath: config.workspacePath, - enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true, - maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default - allowedExtensions: config.allowedExtensions ?? [], - blockedExtensions: config.blockedExtensions ?? [ - '.exe', - '.bat', - '.cmd', - '.sh', - '.ps1', - '.vbs', - '.scr', - '.com', - '.pif', - '.dll', - '.sys', - '.msi', - '.jar' - ] - } - - // Validate workspace path - if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) { - throw new Error('Workspace path must be an absolute path') - } - - // Ensure workspace directory exists - if (!fs.existsSync(this.config.workspacePath)) { - throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`) - } - - // Validate that workspace path is not a sensitive system directory - // This prevents setting workspace to /usr/bin, /etc, etc. which would allow access to system files - if (isSensitiveSystemPath(path.normalize(this.config.workspacePath))) { - throw new Error(`Workspace path cannot be set to sensitive system directory: ${this.config.workspacePath}`) - } - - // Initialize the underlying NodeFileStore with workspace path - this.nodeFileStore = new NodeFileStore(this.config.workspacePath) - } - - /** - * Validates a file path against security policies - * @param filePath The raw user-provided file path (relative to workspace) - * @param resolvedPath The resolved absolute path (for extension validation) - */ - private validateFilePath(filePath: string, resolvedPath: string): void { - // Validate the raw user input for unsafe patterns (path traversal, absolute paths, etc.) - // This must be done on the raw input, not the resolved path, because isUnsafeFilePath - // is designed to detect absolute paths in user input - if (isUnsafeFilePath(filePath)) { - throw new Error(`Unsafe file path detected: ${filePath}`) - } - - // Enforce workspace boundaries if enabled (this handles path resolution internally) - if (this.config.enforceWorkspaceBoundaries) { - if (!isWithinWorkspace(filePath, this.config.workspacePath)) { - throw new Error(`File path outside workspace boundaries: ${filePath}`) - } - } - - // Prevent access to Flowise internal files (any path containing .flowise) - const normalizedResolved = path.normalize(resolvedPath) - if (normalizedResolved.includes('.flowise')) { - throw new Error(`Access to Flowise internal files denied: ${filePath}`) - } - - // Validate that the resolved path does not access sensitive system directories - // This prevents access to system files even if workspace is set to a system directory - if (isSensitiveSystemPath(normalizedResolved)) { - throw new Error(`Access to sensitive system directory denied: ${filePath}`) - } - - // Check file extension on the resolved path to get the actual extension - const ext = path.extname(resolvedPath).toLowerCase() - - // Check blocked extensions - if (this.config.blockedExtensions.includes(ext)) { - throw new Error(`File extension not allowed: ${ext}`) - } - - // Check allowed extensions (if specified) - if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) { - throw new Error(`File extension not in allowed list: ${ext}`) - } - } - - /** - * Validates file size - */ - private validateFileSize(content: string): void { - const sizeInBytes = Buffer.byteLength(content, 'utf8') - if (sizeInBytes > this.config.maxFileSize) { - throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`) - } - } - - /** - * Reads a file with security validation - */ - async readFile(filePath: string): Promise { - // Resolve the full path for extension validation - const resolvedPath = path.resolve(this.config.workspacePath, filePath) - // Validate the raw user input (not the resolved path) to avoid false positives - this.validateFilePath(filePath, resolvedPath) - - try { - return await this.nodeFileStore.readFile(filePath) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to read file: ${path.basename(filePath)}`) - } - } - - /** - * Writes a file with security validation - */ - async writeFile(filePath: string, contents: string): Promise { - this.validateFileSize(contents) - - // Resolve the full path for extension validation and directory creation - const resolvedPath = path.resolve(this.config.workspacePath, filePath) - // Validate the raw user input (not the resolved path) to avoid false positives - this.validateFilePath(filePath, resolvedPath) - - try { - // Ensure the directory exists - const dir = path.dirname(resolvedPath) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - - await this.nodeFileStore.writeFile(filePath, contents) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to write file: ${path.basename(filePath)}`) - } - } - - /** - * Gets the workspace configuration - */ - getConfig(): Readonly> { - return { ...this.config } - } - - /** - * Creates a secure file store with workspace enforcement disabled (for backward compatibility) - * WARNING: This should only be used when absolutely necessary and with proper user consent - */ - static createUnsecure(basePath?: string): SecureFileStore { - const workspacePath = basePath || process.cwd() - return new SecureFileStore({ - workspacePath, - enforceWorkspaceBoundaries: false, - maxFileSize: 50 * 1024 * 1024, // 50MB for insecure mode - blockedExtensions: [] // No extension restrictions in insecure mode - }) - } -} diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index f185f181187..5a72144f0cd 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -69,149 +69,3 @@ export const isUnsafeFilePath = (filePath: string): boolean => { return dangerousPatterns.some((pattern) => pattern.test(filePath)) } - -/** - * Validates if a resolved path accesses sensitive system directories - * Uses pattern-based detection to identify known sensitive system directories - * at root level or one level deep, while allowing legitimate paths like /usr/src - * @param {string} resolvedPath The resolved absolute path to validate - * @returns {boolean} True if path accesses sensitive system directory, false otherwise - */ -export const isSensitiveSystemPath = (resolvedPath: string): boolean => { - if (!resolvedPath || typeof resolvedPath !== 'string') { - return false - } - - // Pattern-based detection for known sensitive system directories: - // Blocks obvious system directories while allowing legitimate paths like /usr/src, /usr/local/src, /opt, etc. - // 1. At root level (e.g., /etc, /sys, /bin, /sbin) - one segment after root - // 2. One level deep (e.g., /etc/passwd, /sys/kernel, /var/log) - two segments total - // 3. Specific sensitive subdirectories (e.g., /var/log, /var/run) - two segments with specific parent - // 4. System binary directories (e.g., /usr/bin, /usr/sbin, /usr/local/bin) - prevents overwriting system executables - const sensitiveSystemPatterns = [ - /^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)([/\\]|$)/i, // Root level: /etc, /sys, /proc, /bin, /sbin, etc. - /^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)[/\\][^/\\]*$/i, // One level deep: /etc/passwd, /sys/kernel, /bin/sh, etc. - /^[/\\]var[/\\](log|run|lib|spool|mail)([/\\]|$)/i, // Sensitive /var subdirectories: /var/log, /var/run, etc. - /^[/\\]usr[/\\](bin|sbin)([/\\]|$)/i, // System binary directories: /usr/bin, /usr/sbin - /^[/\\]usr[/\\]local[/\\](bin|sbin)([/\\]|$)/i // Local system binaries: /usr/local/bin, /usr/local/sbin - ] - - return sensitiveSystemPatterns.some((pattern) => pattern.test(resolvedPath)) -} - -/** - * Validates if a file path is within the allowed workspace boundaries - * @param {string} filePath The file path to validate - * @param {string} workspacePath The workspace base path - * @returns {boolean} True if path is within workspace, false otherwise - */ -export const isWithinWorkspace = (filePath: string, workspacePath: string): boolean => { - if (!filePath || !workspacePath) { - return false - } - - try { - const path = require('path') - - // Resolve both paths to absolute paths - const resolvedFilePath = path.resolve(workspacePath, filePath) - const resolvedWorkspacePath = path.resolve(workspacePath) - - // Normalize paths to handle different separators - const normalizedFilePath = path.normalize(resolvedFilePath) - const normalizedWorkspacePath = path.normalize(resolvedWorkspacePath) - - // Check if the file path starts with the workspace path - const relativePath = path.relative(normalizedWorkspacePath, normalizedFilePath) - - // If relative path starts with '..' or is absolute, it's outside workspace - return !relativePath.startsWith('..') && !path.isAbsolute(relativePath) - } catch (error) { - // If any error occurs during path resolution, deny access - return false - } -} - -/** - * Validates if a browser executable path is safe to use - * Prevents arbitrary code execution through environment variable manipulation - * @param {string} executablePath The browser executable path to validate - * @returns {boolean} True if path is safe, false otherwise - */ -export const isSafeBrowserExecutable = (executablePath: string | undefined): boolean => { - if (!executablePath) { - return true // If not specified, let browser library use its default - } - - if (typeof executablePath !== 'string' || executablePath.trim() === '') { - return false - } - - const path = require('path') - const fs = require('fs') - - try { - // Normalize the path - const normalizedPath = path.normalize(executablePath) - - // Must be an absolute path - if (!path.isAbsolute(normalizedPath)) { - return false - } - - // Allowed browser executable locations (system-managed only) - const allowedPaths = [ - // Linux/Unix Chromium/Chrome paths - '/usr/bin/chromium', - '/usr/bin/chromium-browser', - '/usr/bin/google-chrome', - '/usr/bin/google-chrome-stable', - '/usr/bin/chrome', - '/snap/bin/chromium', - // macOS Chrome/Chromium paths - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - '/Applications/Chromium.app/Contents/MacOS/Chromium', - // Windows Chrome/Chromium paths (normalized with forward slashes) - 'C:/Program Files/Google/Chrome/Application/chrome.exe', - 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', - 'C:/Program Files/Chromium/Application/chrome.exe', - // Firefox paths - '/usr/bin/firefox', - '/Applications/Firefox.app/Contents/MacOS/firefox', - 'C:/Program Files/Mozilla Firefox/firefox.exe', - 'C:/Program Files (x86)/Mozilla Firefox/firefox.exe' - ] - - // Normalize allowed paths for comparison (handle Windows backslashes) - const normalizedAllowedPaths = allowedPaths.map((p) => path.normalize(p)) - - // Check if the path exactly matches one of the allowed paths - const isAllowedPath = normalizedAllowedPaths.some((allowedPath) => normalizedPath.toLowerCase() === allowedPath.toLowerCase()) - - if (!isAllowedPath) { - return false - } - - // Additional security: Verify file exists and is executable (where applicable) - // This prevents using a path before malicious file is written - try { - if (fs.existsSync(normalizedPath)) { - const stats = fs.statSync(normalizedPath) - // On Unix-like systems, check if file is executable - if (process.platform !== 'win32') { - // Check if file has execute permissions (using bitwise AND) - // 0o111 checks for execute permission for user, group, or others - return (stats.mode & 0o111) !== 0 - } - return stats.isFile() - } - // If file doesn't exist, reject it (prevents race conditions) - return false - } catch { - return false - } - } catch (error) { - // If any error occurs during validation, deny access - return false - } -} From 75e0f75414b5226cb25942074ced2b0183ad320f Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 20:57:42 +0000 Subject: [PATCH 19/25] Bugfix/Custom Function Libraries (#5472) * Updated the executeJavaScriptCode function to automatically detect and install required libraries from import/require statements in the provided code. * Update utils.ts * lint-fix --- packages/components/src/utils.ts | 24 +++++++++++++++++-- .../marketplaces/agentflowsv2/SQL Agent.json | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 7c526681f5e..fef9adac407 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1500,9 +1500,29 @@ export const executeJavaScriptCode = async ( const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs }) + // Determine which libraries to install + const librariesToInstall = new Set(libraries) + + // Auto-detect required libraries from code + // Extract required modules from import/require statements + const importRegex = /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g + let match + while ((match = importRegex.exec(code)) !== null) { + const moduleName = match[1] || match[2] + // Extract base module name (e.g., 'typeorm' from 'typeorm/something') + const baseModuleName = moduleName.split('/')[0] + librariesToInstall.add(baseModuleName) + } + // Install libraries - for (const library of libraries) { - await sbx.commands.run(`npm install ${library}`) + for (const library of librariesToInstall) { + // Validate library name to prevent command injection. + const validPackageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/ + if (validPackageNameRegex.test(library)) { + await sbx.commands.run(`npm install ${library}`) + } else { + console.warn(`[Sandbox] Skipping installation of invalid module: ${library}`) + } } // Separate imports from the rest of the code for proper ES6 module structure diff --git a/packages/server/marketplaces/agentflowsv2/SQL Agent.json b/packages/server/marketplaces/agentflowsv2/SQL Agent.json index 02af729b82a..70d1bba5cf8 100644 --- a/packages/server/marketplaces/agentflowsv2/SQL Agent.json +++ b/packages/server/marketplaces/agentflowsv2/SQL Agent.json @@ -284,7 +284,7 @@ "inputAnchors": [], "inputs": { "customFunctionInputVariables": "", - "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\n\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nlet sqlSchemaPrompt = '';\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nasync function getSQLPrompt() {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n // Get all user-defined tables (excluding system tables)\n const tablesResult = await queryRunner.query(`\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `);\n\n for (const tableRow of tablesResult) {\n const tableName = tableRow.table_name;\n\n const schemaInfo = await queryRunner.query(`\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}'\n `);\n\n const createColumns = [];\n const columnNames = [];\n\n for (const column of schemaInfo) {\n const name = column.column_name;\n const type = column.data_type.toUpperCase();\n const notNull = column.is_nullable === 'NO' ? 'NOT NULL' : '';\n columnNames.push(name);\n createColumns.push(`${name} ${type} ${notNull}`);\n }\n\n const sqlCreateTableQuery = `CREATE TABLE ${tableName} (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM ${tableName} LIMIT 3`;\n\n let allValues = [];\n try {\n const rows = await queryRunner.query(sqlSelectTableQuery);\n\n allValues = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n } catch (err) {\n allValues.push('[ERROR FETCHING ROWS]');\n }\n\n sqlSchemaPrompt +=\n sqlCreateTableQuery +\n '\\n' +\n sqlSelectTableQuery +\n '\\n' +\n columnNames.join(' ') +\n '\\n' +\n allValues.join('\\n') +\n '\\n\\n';\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error(err);\n throw err;\n }\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;\n", + "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\nconst { Pool } = require('pg');\n\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nlet sqlSchemaPrompt = '';\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nasync function getSQLPrompt() {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n // Get all user-defined tables (excluding system tables)\n const tablesResult = await queryRunner.query(`\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `);\n\n for (const tableRow of tablesResult) {\n const tableName = tableRow.table_name;\n\n const schemaInfo = await queryRunner.query(`\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}'\n `);\n\n const createColumns = [];\n const columnNames = [];\n\n for (const column of schemaInfo) {\n const name = column.column_name;\n const type = column.data_type.toUpperCase();\n const notNull = column.is_nullable === 'NO' ? 'NOT NULL' : '';\n columnNames.push(name);\n createColumns.push(`${name} ${type} ${notNull}`);\n }\n\n const sqlCreateTableQuery = `CREATE TABLE ${tableName} (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM ${tableName} LIMIT 3`;\n\n let allValues = [];\n try {\n const rows = await queryRunner.query(sqlSelectTableQuery);\n\n allValues = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n } catch (err) {\n allValues.push('[ERROR FETCHING ROWS]');\n }\n\n sqlSchemaPrompt +=\n sqlCreateTableQuery +\n '\\n' +\n sqlSelectTableQuery +\n '\\n' +\n columnNames.join(' ') +\n '\\n' +\n allValues.join('\\n') +\n '\\n\\n';\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error(err);\n throw err;\n }\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;\n", "customFunctionUpdateState": "" }, "outputAnchors": [ @@ -913,7 +913,7 @@ "variableValue": "

{{ $flow.state.sqlQuery }}

" } ], - "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\n\n// Configuration\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nconst sqlQuery = $sqlQuery;\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nlet formattedResult = '';\n\nasync function runSQLQuery(query) {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n const rows = await queryRunner.query(query);\n console.log('rows =', rows);\n\n if (rows.length === 0) {\n formattedResult = '[No results returned]';\n } else {\n const columnNames = Object.keys(rows[0]);\n const header = columnNames.join(' ');\n const values = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n\n formattedResult = query + '\\n' + header + '\\n' + values.join('\\n');\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error('[ERROR]', err);\n formattedResult = `[Error executing query]: ${err}`;\n }\n\n return formattedResult;\n}\n\nasync function main() {\n formattedResult = await runSQLQuery(sqlQuery);\n}\n\nawait main();\n\nreturn formattedResult;\n", + "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\nconst { Pool } = require('pg');\n\n// Configuration\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nconst sqlQuery = $sqlQuery;\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nlet formattedResult = '';\n\nasync function runSQLQuery(query) {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n const rows = await queryRunner.query(query);\n console.log('rows =', rows);\n\n if (rows.length === 0) {\n formattedResult = '[No results returned]';\n } else {\n const columnNames = Object.keys(rows[0]);\n const header = columnNames.join(' ');\n const values = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n\n formattedResult = query + '\\n' + header + '\\n' + values.join('\\n');\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error('[ERROR]', err);\n formattedResult = `[Error executing query]: ${err}`;\n }\n\n return formattedResult;\n}\n\nasync function main() {\n formattedResult = await runSQLQuery(sqlQuery);\n}\n\nawait main();\n\nreturn formattedResult;\n", "customFunctionUpdateState": "" }, "outputAnchors": [ From 8e81a4396aa41e388ced5ba600c204d1763adbb9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 23:11:40 +0000 Subject: [PATCH 20/25] Release/3.0.11 (#5481) flowise@3.0.11 --- packages/components/package.json | 2 +- packages/server/package.json | 2 +- packages/ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 9f1ab13f738..f3894f996c4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "3.0.10", + "version": "3.0.11", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", diff --git a/packages/server/package.json b/packages/server/package.json index 50fc00d03fc..0427f186c99 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "3.0.10", + "version": "3.0.11", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 6d1edef7ede..75e532fcd1f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "3.0.10", + "version": "3.0.11", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From ff0f3f19b8f3bd288ecc53afe6c82659114c808c Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 15 Nov 2025 23:13:31 +0000 Subject: [PATCH 21/25] flowise@3.0.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08d32de876c..9ee93d127b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "10", + "version": "3.0.11", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ From 96c9cf0d57e02c6dba83d0537b776e342de218cf Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sun, 16 Nov 2025 13:17:59 +0000 Subject: [PATCH 22/25] Chore/Disable Unstructure Folder (#5483) * commented out unstructure folder node * Update packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../nodes/documentloaders/Unstructured/Unstructured.ts | 4 +--- .../nodes/documentloaders/Unstructured/UnstructuredFolder.ts | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts b/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts index 8f3e49f6258..2d976bfb8d7 100644 --- a/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts +++ b/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts @@ -27,8 +27,6 @@ type Element = { } export class UnstructuredLoader extends BaseDocumentLoader { - public filePath: string - private apiUrl = process.env.UNSTRUCTURED_API_URL || 'https://api.unstructuredapp.io/general/v0/general' private apiKey: string | undefined = process.env.UNSTRUCTURED_API_KEY @@ -138,7 +136,7 @@ export class UnstructuredLoader extends BaseDocumentLoader { }) if (!response.ok) { - throw new Error(`Failed to partition file ${this.filePath} with error ${response.status} and message ${await response.text()}`) + throw new Error(`Failed to partition file with error ${response.status} and message ${await response.text()}`) } const elements = await response.json() diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts index efa20caf189..1e217a65ede 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts @@ -1,3 +1,6 @@ +/* +* Uncomment this if you want to use the UnstructuredFolder to load a folder from the file system + import { omit } from 'lodash' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { @@ -516,3 +519,4 @@ class UnstructuredFolder_DocumentLoaders implements INode { } module.exports = { nodeClass: UnstructuredFolder_DocumentLoaders } +*/ From 3ee26268fdbff08bce8e56954b4b6712c71603e4 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Wed, 19 Nov 2025 23:18:19 +0530 Subject: [PATCH 23/25] update: condition for handling 429 errors --- packages/ui/src/store/context/ErrorContext.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index 9201242acf4..0d5cf181076 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -14,7 +14,7 @@ export const ErrorProvider = ({ children }) => { const handleError = async (err) => { console.error(err) - if (err?.response?.status === 429) { + if (err?.response?.status === 429 && err?.response?.data?.type !== 'authentication_rate_limit') { const retryAfterHeader = err?.response?.headers?.['retry-after'] let retryAfter = 60 // Default in seconds if (retryAfterHeader) { From 4ece068584711ddb9917cf5b57eab9c74fb1d55c Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Wed, 26 Nov 2025 15:25:44 +0530 Subject: [PATCH 24/25] update: handle rate limit errors in auth pages --- packages/ui/src/store/context/ErrorContext.jsx | 9 +++++++-- packages/ui/src/views/auth/forgotPassword.jsx | 14 ++++++++++++++ packages/ui/src/views/auth/register.jsx | 12 +++++++++++- packages/ui/src/views/auth/resetPassword.jsx | 16 ++++++++++++++++ packages/ui/src/views/auth/signIn.jsx | 12 +++++++++++- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index 0d5cf181076..8f97bb98089 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -10,11 +10,14 @@ const ErrorContext = createContext() export const ErrorProvider = ({ children }) => { const [error, setError] = useState(null) + const [authRateLimitError, setAuthRateLimitError] = useState(null) const navigate = useNavigate() const handleError = async (err) => { console.error(err) - if (err?.response?.status === 429 && err?.response?.data?.type !== 'authentication_rate_limit') { + if (err?.response?.status === 429 && err?.response?.data?.type === 'authentication_rate_limit') { + setAuthRateLimitError("You're making a lot of requests. Please wait and try again later.") + } else if (err?.response?.status === 429 && err?.response?.data?.type !== 'authentication_rate_limit') { const retryAfterHeader = err?.response?.headers?.['retry-after'] let retryAfter = 60 // Default in seconds if (retryAfterHeader) { @@ -59,7 +62,9 @@ export const ErrorProvider = ({ children }) => { value={{ error, setError, - handleError + handleError, + authRateLimitError, + setAuthRateLimitError }} > {children} diff --git a/packages/ui/src/views/auth/forgotPassword.jsx b/packages/ui/src/views/auth/forgotPassword.jsx index 9e17f243617..7e375a1253c 100644 --- a/packages/ui/src/views/auth/forgotPassword.jsx +++ b/packages/ui/src/views/auth/forgotPassword.jsx @@ -16,6 +16,7 @@ import accountApi from '@/api/account.api' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // utils import useNotifier from '@/utils/useNotifier' @@ -41,10 +42,13 @@ const ForgotPasswordPage = () => { const [isLoading, setLoading] = useState(false) const [responseMsg, setResponseMsg] = useState(undefined) + const { authRateLimitError, setAuthRateLimitError } = useError() + const forgotPasswordApi = useApi(accountApi.forgotPassword) const sendResetRequest = async (event) => { event.preventDefault() + setAuthRateLimitError(null) const body = { user: { email: usernameVal @@ -54,6 +58,11 @@ const ForgotPasswordPage = () => { await forgotPasswordApi.request(body) } + useEffect(() => { + setAuthRateLimitError(null) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setAuthRateLimitError]) + useEffect(() => { if (forgotPasswordApi.error) { const errMessage = @@ -89,6 +98,11 @@ const ForgotPasswordPage = () => { {responseMsg.msg} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {responseMsg && responseMsg?.type !== 'error' && ( } variant='filled' severity='success'> {responseMsg.msg} diff --git a/packages/ui/src/views/auth/register.jsx b/packages/ui/src/views/auth/register.jsx index 30c18b12e1f..3273f3402c3 100644 --- a/packages/ui/src/views/auth/register.jsx +++ b/packages/ui/src/views/auth/register.jsx @@ -18,6 +18,7 @@ import ssoApi from '@/api/sso' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // utils import useNotifier from '@/utils/useNotifier' @@ -111,7 +112,9 @@ const RegisterPage = () => { const [loading, setLoading] = useState(false) const [authError, setAuthError] = useState('') - const [successMsg, setSuccessMsg] = useState(undefined) + const [successMsg, setSuccessMsg] = useState('') + + const { authRateLimitError, setAuthRateLimitError } = useError() const registerApi = useApi(accountApi.registerAccount) const ssoLoginApi = useApi(ssoApi.ssoLogin) @@ -120,6 +123,7 @@ const RegisterPage = () => { const register = async (event) => { event.preventDefault() + setAuthRateLimitError(null) if (isEnterpriseLicensed) { const result = RegisterEnterpriseUserSchema.safeParse({ username, @@ -192,6 +196,7 @@ const RegisterPage = () => { }, [registerApi.error]) useEffect(() => { + setAuthRateLimitError(null) if (!isOpenSource) { getDefaultProvidersApi.request() } @@ -274,6 +279,11 @@ const RegisterPage = () => { )} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {successMsg && ( } variant='filled' severity='success'> {successMsg} diff --git a/packages/ui/src/views/auth/resetPassword.jsx b/packages/ui/src/views/auth/resetPassword.jsx index 3ca33f8cdb8..77ec485639b 100644 --- a/packages/ui/src/views/auth/resetPassword.jsx +++ b/packages/ui/src/views/auth/resetPassword.jsx @@ -19,6 +19,9 @@ import accountApi from '@/api/account.api' import useNotifier from '@/utils/useNotifier' import { validatePassword } from '@/utils/validation' +// Hooks +import { useError } from '@/store/context/ErrorContext' + // Icons import { IconExclamationCircle, IconX } from '@tabler/icons-react' @@ -70,6 +73,8 @@ const ResetPasswordPage = () => { const [loading, setLoading] = useState(false) const [authErrors, setAuthErrors] = useState([]) + const { authRateLimitError, setAuthRateLimitError } = useError() + const goLogin = () => { navigate('/signin', { replace: true }) } @@ -78,6 +83,7 @@ const ResetPasswordPage = () => { event.preventDefault() const validationErrors = [] setAuthErrors([]) + setAuthRateLimitError(null) if (!tokenVal) { validationErrors.push('Token cannot be left blank!') } @@ -142,6 +148,11 @@ const ResetPasswordPage = () => { } } + useEffect(() => { + setAuthRateLimitError(null) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( <> @@ -155,6 +166,11 @@ const ResetPasswordPage = () => { )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} Reset Password diff --git a/packages/ui/src/views/auth/signIn.jsx b/packages/ui/src/views/auth/signIn.jsx index 1e0a7d3cf46..f097cd425d4 100644 --- a/packages/ui/src/views/auth/signIn.jsx +++ b/packages/ui/src/views/auth/signIn.jsx @@ -14,6 +14,7 @@ import { Input } from '@/ui-component/input/Input' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // API import authApi from '@/api/auth' @@ -62,6 +63,8 @@ const SignInPage = () => { const [showResendButton, setShowResendButton] = useState(false) const [successMessage, setSuccessMessage] = useState('') + const { authRateLimitError, setAuthRateLimitError } = useError() + const loginApi = useApi(authApi.login) const ssoLoginApi = useApi(ssoApi.ssoLogin) const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods) @@ -71,6 +74,7 @@ const SignInPage = () => { const doLogin = (event) => { event.preventDefault() + setAuthRateLimitError(null) setLoading(true) const body = { email: usernameVal, @@ -92,11 +96,12 @@ const SignInPage = () => { useEffect(() => { store.dispatch(logoutSuccess()) + setAuthRateLimitError(null) if (!isOpenSource) { getDefaultProvidersApi.request() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [setAuthRateLimitError, isOpenSource]) useEffect(() => { // Parse the "user" query parameter from the URL @@ -179,6 +184,11 @@ const SignInPage = () => { {successMessage} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {authError && ( } variant='filled' severity='error'> {authError} From 78bc7334da1a8ce7f68fbd36b228cf151912abff Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Thu, 27 Nov 2025 14:07:57 +0530 Subject: [PATCH 25/25] fix: crash due to missing import --- packages/ui/src/views/auth/resetPassword.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/auth/resetPassword.jsx b/packages/ui/src/views/auth/resetPassword.jsx index 77ec485639b..f451b8a20bd 100644 --- a/packages/ui/src/views/auth/resetPassword.jsx +++ b/packages/ui/src/views/auth/resetPassword.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { Link, useNavigate, useSearchParams } from 'react-router-dom'