From a7b6f9b208dc4509c257a2978e36ab68ea525c12 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 6 Nov 2025 23:00:25 +0000 Subject: [PATCH 01/18] Chore/split docker workflows (#5432) * refactor: Split Docker image CI workflow into registry-specific workflows * refactor: Split Docker image CI workflow into registry-specific workflows --- .github/workflows/docker-image-dockerhub.yml | 72 ++++++++++++ .github/workflows/docker-image-ecr.yml | 73 ++++++++++++ .github/workflows/docker-image.yml | 114 ------------------- 3 files changed, 145 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/docker-image-dockerhub.yml create mode 100644 .github/workflows/docker-image-ecr.yml delete mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image-dockerhub.yml b/.github/workflows/docker-image-dockerhub.yml new file mode 100644 index 00000000000..3752ddc7ed4 --- /dev/null +++ b/.github/workflows/docker-image-dockerhub.yml @@ -0,0 +1,72 @@ +name: Docker Image CI - Docker Hub + +on: + workflow_dispatch: + inputs: + node_version: + description: 'Node.js version to build this image with.' + type: choice + required: true + default: '20' + options: + - '20' + tag_version: + description: 'Tag version of the image to be pushed.' + type: string + required: true + default: 'latest' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set default values + id: defaults + run: | + echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT + echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # ------------------------- + # Build and push main image + # ------------------------- + - name: Build and push main image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: ./docker/Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + flowiseai/flowise:${{ steps.defaults.outputs.tag_version }} + + # ------------------------- + # Build and push worker image + # ------------------------- + - name: Build and push worker image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: docker/worker/Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + flowiseai/flowise-worker:${{ steps.defaults.outputs.tag_version }} diff --git a/.github/workflows/docker-image-ecr.yml b/.github/workflows/docker-image-ecr.yml new file mode 100644 index 00000000000..1fc28fb1d8b --- /dev/null +++ b/.github/workflows/docker-image-ecr.yml @@ -0,0 +1,73 @@ +name: Docker Image CI - AWS ECR + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to push the image to.' + required: true + default: 'dev' + type: choice + options: + - dev + - prod + node_version: + description: 'Node.js version to build this image with.' + type: choice + required: true + default: '20' + options: + - '20' + tag_version: + description: 'Tag version of the image to be pushed.' + type: string + required: true + default: 'latest' + +jobs: + docker: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + steps: + - name: Set default values + id: defaults + run: | + echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT + echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v1 + + # ------------------------- + # Build and push main image + # ------------------------- + - name: Build and push main image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ format('{0}.dkr.ecr.{1}.amazonaws.com/flowise:{2}', + secrets.AWS_ACCOUNT_ID, + secrets.AWS_REGION, + steps.defaults.outputs.tag_version) }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 7faf472f59b..00000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: Docker Image CI - -on: - workflow_dispatch: - inputs: - registry: - description: 'Container Registry to push the image to.' - type: choice - required: true - default: 'aws_ecr' - options: - - 'docker_hub' - - 'aws_ecr' - environment: - description: 'Environment to push the image to.' - required: true - default: 'dev' - type: choice - options: - - dev - - prod - image_type: - description: 'Type of image to build and push.' - type: choice - required: true - default: 'main' - options: - - 'main' - - 'worker' - node_version: - description: 'Node.js version to build this image with.' - type: choice - required: true - default: '20' - options: - - '20' - tag_version: - description: 'Tag version of the image to be pushed.' - type: string - required: true - default: 'latest' - -jobs: - docker: - runs-on: ubuntu-latest - environment: ${{ github.event.inputs.environment }} - steps: - - name: Set default values - id: defaults - run: | - echo "registry=${{ github.event.inputs.registry || 'aws_ecr' }}" >> $GITHUB_OUTPUT - echo "image_type=${{ github.event.inputs.image_type || 'main' }}" >> $GITHUB_OUTPUT - echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT - echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT - - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 - - # ------------------------ - # Login Steps (conditional) - # ------------------------ - - name: Login to Docker Hub - if: steps.defaults.outputs.registry == 'docker_hub' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Configure AWS Credentials - if: steps.defaults.outputs.registry == 'aws_ecr' - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Login to Amazon ECR - if: steps.defaults.outputs.registry == 'aws_ecr' - uses: aws-actions/amazon-ecr-login@v1 - - # ------------------------- - # Build and push (conditional tags) - # ------------------------- - - name: Build and push - uses: docker/build-push-action@v5.3.0 - with: - context: . - file: | - ${{ - steps.defaults.outputs.image_type == 'worker' && 'docker/worker/Dockerfile' || - (steps.defaults.outputs.registry == 'docker_hub' && './docker/Dockerfile' || 'Dockerfile') - }} - build-args: | - NODE_VERSION=${{ steps.defaults.outputs.node_version }} - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ - steps.defaults.outputs.registry == 'docker_hub' && - format('flowiseai/flowise{0}:{1}', - steps.defaults.outputs.image_type == 'worker' && '-worker' || '', - steps.defaults.outputs.tag_version) || - format('{0}.dkr.ecr.{1}.amazonaws.com/flowise{2}:{3}', - secrets.AWS_ACCOUNT_ID, - secrets.AWS_REGION, - steps.defaults.outputs.image_type == 'worker' && '-worker' || '', - steps.defaults.outputs.tag_version) - }} From 4624e15c2ec615f83938de8fa41b2dcbd6f19c57 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 02/18] 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 faf0a0a3151bfae3b678d75ec80dfb832fad98ca 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 03/18] 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 6d3755d16e77e135a81d5c84b4287398a29af55e 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 04/18] 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 761ffe68519c2199b20fa7c3c5a707a1988e8aeb 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 05/18] 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 b9a020dc70b0a114e5720fd173d842a373cf0147 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 06/18] 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 0dc14b5cd3184e34b5f5d59bd65855f661965ac9 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 07/18] 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 9ff3d653aee15d4e3c812bc2047b13d37f105359 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 08/18] 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 3fafd15a8001cad7c75efe7824d72ac4c0e084b6 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 09/18] 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 94cae3b66fdcb8926cebb86f3530bfda6cad2acf Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 7 Nov 2025 11:51:54 +0000 Subject: [PATCH 10/18] 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 ceb0512e2f494d9f9ff72c696833c40ab57773f9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 13 Nov 2025 11:11:39 +0000 Subject: [PATCH 11/18] 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 4a642f02d0eec2d945d15c6d2a733fb069ad9d9c Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 11:16:42 +0000 Subject: [PATCH 12/18] 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 2414057c08e05b03666da400911086b8ce0dfe6d Mon Sep 17 00:00:00 2001 From: Taraka Vishnumolakala Date: Sat, 15 Nov 2025 10:03:01 -0500 Subject: [PATCH 13/18] =?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 366d38b861052c3e5f19d6ba6bda787eb08fa55d Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 18:09:31 +0000 Subject: [PATCH 14/18] 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 3cab803918f7bdcadb1cc94e1f9b4bf374c9b629 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 20:24:42 +0000 Subject: [PATCH 15/18] 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 888994bc8f6dcbcc13c5df12e652100e60ee1a6f Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 20:57:42 +0000 Subject: [PATCH 16/18] 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 4e1fac501fc1a2f62ad40800f17a4e0462c39f9e Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 15 Nov 2025 23:11:40 +0000 Subject: [PATCH 17/18] 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 2f2b6e1713cd775e2f4d7458c0ce8e912fe53a84 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 15 Nov 2025 23:13:31 +0000 Subject: [PATCH 18/18] 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": [