diff --git a/docker/.env.example b/docker/.env.example index f40572a5ea8..9221070b44d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -41,7 +41,7 @@ PORT=3000 # LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials # LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs -# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash,pg,mysql2,mongodb,ioredis,redis,typeorm,puppeteer,playwright,@zilliz/milvus2-sdk-node # ALLOW_BUILTIN_DEP=false diff --git a/docker/worker/.env.example b/docker/worker/.env.example index 3f92375bee6..005ffa79499 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -41,7 +41,7 @@ WORKER_PORT=5566 # LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials # LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs -# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash,pg,mysql2,mongodb,ioredis,redis,typeorm,puppeteer,playwright,@zilliz/milvus2-sdk-node # ALLOW_BUILTIN_DEP=false diff --git a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts index 7987ad4cc90..dc48968b8ab 100644 --- a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts +++ b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts @@ -10,6 +10,7 @@ import { import { AxiosRequestConfig } from 'axios' import { secureAxiosRequest } from '../../../src/httpSecurity' import { getCredentialData, getCredentialParam, processTemplateVariables, parseJsonBody } from '../../../src/utils' +import { isValidURL } from '../../../src/validator' import { DataSource } from 'typeorm' import { BaseMessageLike } from '@langchain/core/messages' import { updateFlowState } from '../utils' @@ -183,6 +184,8 @@ class ExecuteFlow_Agentflow implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData) + if (!baseURL || !isValidURL(baseURL)) throw new Error('Invalid base URL: must be a valid URL') + if (selectedFlowId === options.chatflowid) throw new Error('Cannot call the same agentflow!') let headers: Record = { diff --git a/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts b/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts index e3cea9a173f..be64a429575 100644 --- a/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts +++ b/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts @@ -239,19 +239,20 @@ class ExecuteFlow_SeqAgents implements INode { // Create additional sandbox variables const additionalSandbox: ICommonObject = { $callOptions: callOptions, - $callBody: body + $callBody: body, + $apiURL: `${baseURL}/api/v1/prediction/${selectedFlowId}` } const sandbox = createCodeExecutionSandbox(flowInput, variables, flow, additionalSandbox) const code = ` const fetch = require('node-fetch'); - const url = "${baseURL}/api/v1/prediction/${selectedFlowId}"; - + const url = $apiURL; + const body = $callBody; - + const options = $callOptions; - + try { const response = await fetch(url, options); const resp = await response.json(); diff --git a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts index 0565c0caf73..a3a80e101da 100644 --- a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts +++ b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts @@ -345,7 +345,7 @@ class AgentflowTool extends StructuredTool { const code = ` const fetch = require('node-fetch'); -const url = "${this.baseURL}/api/v1/prediction/${this.agentflowid}"; +const url = $apiURL; const body = $callBody; @@ -364,7 +364,8 @@ try { // Create additional sandbox variables const additionalSandbox: ICommonObject = { $callOptions: options, - $callBody: body + $callBody: body, + $apiURL: `${this.baseURL}/api/v1/prediction/${this.agentflowid}` } const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox) diff --git a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts index fa9d463aec1..a2ae7cc8172 100644 --- a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts +++ b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts @@ -353,7 +353,7 @@ class ChatflowTool extends StructuredTool { const code = ` const fetch = require('node-fetch'); -const url = "${this.baseURL}/api/v1/prediction/${this.chatflowid}"; +const url = $apiURL; const body = $callBody; @@ -372,7 +372,8 @@ try { // Create additional sandbox variables const additionalSandbox: ICommonObject = { $callOptions: options, - $callBody: body + $callBody: body, + $apiURL: `${this.baseURL}/api/v1/prediction/${this.chatflowid}` } const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox) diff --git a/packages/components/src/utils.test.ts b/packages/components/src/utils.test.ts index ba8238ee049..97fa0842627 100644 --- a/packages/components/src/utils.test.ts +++ b/packages/components/src/utils.test.ts @@ -1,4 +1,10 @@ -import { removeInvalidImageMarkdown, convertRequireToImport, COMMONJS_REQUIRE_REGEX, IMPORT_EXTRACTION_REGEX } from './utils' +import { + removeInvalidImageMarkdown, + convertRequireToImport, + COMMONJS_REQUIRE_REGEX, + IMPORT_EXTRACTION_REGEX, + executeJavaScriptCode +} from './utils' describe('removeInvalidImageMarkdown', () => { describe('strips non-http/https image markdown', () => { @@ -229,3 +235,55 @@ describe('Import extraction regex (utils.ts line 1596 pattern)', () => { expect(extractModules('console.log("hello")')).toEqual([]) }) }) + +// --------------------------------------------------------------------------- +// NodeVM sandbox — availableDependencies allowlist +// --------------------------------------------------------------------------- + +describe('NodeVM sandbox — availableDependencies allowlist', () => { + afterEach(() => { + delete process.env.ALLOW_BUILTIN_DEP + delete process.env.TOOL_FUNCTION_EXTERNAL_DEP + }) + + describe('high-risk packages are blocked even when ALLOW_BUILTIN_DEP=true', () => { + beforeEach(() => { + process.env.ALLOW_BUILTIN_DEP = 'true' + }) + + const removedPackages = [ + 'pg', + 'mysql2', + 'mongodb', + 'ioredis', + 'redis', + 'typeorm', + 'puppeteer', + 'playwright', + '@zilliz/milvus2-sdk-node' + ] + + test.each(removedPackages)( + "require('%s') is denied", + async (pkg) => { + await expect( + executeJavaScriptCode(`const m = require('${pkg}'); return 'loaded'`, {}, { timeout: 10000 }) + ).rejects.toThrow() + }, + 15000 + ) + }) + + it('packages remaining in availableDependencies are still accessible with ALLOW_BUILTIN_DEP=true', async () => { + process.env.ALLOW_BUILTIN_DEP = 'true' + const result = await executeJavaScriptCode(`const cheerio = require('cheerio'); return typeof cheerio.load`, {}, { timeout: 10000 }) + expect(result).toBe('function') + }, 15000) + + it('a removed package becomes accessible via TOOL_FUNCTION_EXTERNAL_DEP', async () => { + process.env.ALLOW_BUILTIN_DEP = 'true' + process.env.TOOL_FUNCTION_EXTERNAL_DEP = 'pg' + const result = await executeJavaScriptCode(`const { Client } = require('pg'); return typeof Client`, {}, { timeout: 10000 }) + expect(result).toBe('function') + }, 15000) +}) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 647ec77abf3..699f009abd3 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -86,7 +86,6 @@ export const availableDependencies = [ '@qdrant/js-client-rest', '@supabase/supabase-js', '@upstash/redis', - '@zilliz/milvus2-sdk-node', 'apify-client', 'cheerio', 'chromadb', @@ -97,7 +96,6 @@ export const availableDependencies = [ 'google-auth-library', 'graphql', 'html-to-text', - 'ioredis', 'langchain', 'langfuse', 'langsmith', @@ -105,24 +103,17 @@ export const availableDependencies = [ 'linkifyjs', 'lunary', 'mammoth', - 'mongodb', - 'mysql2', 'node-html-markdown', 'notion-to-md', 'openai', 'pdf-parse', 'pdfjs-dist', - 'pg', - 'playwright', - 'puppeteer', - 'redis', 'replicate', 'srt-parser-2', - 'typeorm', 'weaviate-client' ] -const defaultAllowExternalDependencies = ['axios', 'moment', 'node-fetch'] +const defaultAllowExternalDependencies = ['axios', 'node-fetch'] export const defaultAllowBuiltInDep = ['assert', 'buffer', 'crypto', 'events', 'path', 'querystring', 'timers', 'url', 'zlib'] @@ -1780,6 +1771,7 @@ export const executeJavaScriptCode = async ( }, eval: false, wasm: false, + fixAsync: true, timeout: timeoutMs } @@ -1789,7 +1781,8 @@ export const executeJavaScriptCode = async ( ...nodeVMOptions, require: defaultNodeVMOptions.require, eval: false, - wasm: false + wasm: false, + fixAsync: true } const vm = new NodeVM(finalNodeVMOptions) diff --git a/packages/components/src/validator.test.ts b/packages/components/src/validator.test.ts index 44b3af7d40e..6e81ffdd2d6 100644 --- a/packages/components/src/validator.test.ts +++ b/packages/components/src/validator.test.ts @@ -1,4 +1,4 @@ -import { isPathTraversal, isUnsafeFilePath, validateMimeTypeAndExtensionMatch, validateVectorStorePath } from './validator' +import { isPathTraversal, isUnsafeFilePath, isValidURL, validateMimeTypeAndExtensionMatch, validateVectorStorePath } from './validator' import path from 'path' import { getUserHome } from './utils' @@ -466,3 +466,61 @@ describe('validateVectorStorePath', () => { }) }) }) + +describe('isValidURL', () => { + describe('accepts valid http/https URLs', () => { + it.each([ + ['bare http host', 'http://localhost:3000'], + ['https with path', 'https://flowise.example.com/api'], + ['http with port and path', 'http://192.168.1.1:3000/api/v1'], + ['https with query string', 'https://example.com/search?q=hello'] + ])('should accept %s', (_desc, url) => { + expect(isValidURL(url)).toBe(true) + }) + }) + + describe('rejects non-http(s) protocols', () => { + it.each([ + ['file protocol', 'file:///etc/passwd'], + ['javascript protocol', 'javascript:alert(1)'], + ['ftp protocol', 'ftp://example.com'], + ['data URI', 'data:text/html,'] + ])('should reject %s', (_desc, url) => { + expect(isValidURL(url)).toBe(false) + }) + }) + + describe('rejects URLs with hash fragments (CVE-2022-24785 bypass entry point)', () => { + it.each([ + ['plain hash', 'http://localhost:3000/#section'], + ['hash with injection payload', 'https://evil.com/#";\nrequire("child_process").exec("id");//'], + ['hash with quote escape', 'http://localhost:3000/#";malicious;//'] + ])('should reject %s', (_desc, url) => { + expect(isValidURL(url)).toBe(false) + }) + }) + + describe('rejects URLs containing JS string-breaking characters', () => { + it.each([ + ['double quote', 'http://localhost:3000/path"suffix'], + ['single quote', "http://localhost:3000/path'suffix"], + ['backtick', 'http://localhost:3000/path`suffix'], + ['backslash', 'http://localhost:3000/path\\suffix'], + ['newline', 'http://localhost:3000/path\nsuffix'], + ['carriage return', 'http://localhost:3000/path\rsuffix'], + ['tab', 'http://localhost:3000/path\tsuffix'] + ])('should reject URL with %s', (_desc, url) => { + expect(isValidURL(url)).toBe(false) + }) + }) + + describe('rejects malformed or empty inputs', () => { + it.each([ + ['empty string', ''], + ['not a URL', 'not-a-url'], + ['relative path', '/api/v1/prediction/abc'] + ])('should reject %s', (_desc, url) => { + expect(isValidURL(url)).toBe(false) + }) + }) +}) diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index 42ef4bf9413..45392a0e5dc 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -14,13 +14,17 @@ export const isValidUUID = (uuid: string): boolean => { } /** - * Validates if a string is a valid URL - * @param {string} url The string to validate - * @returns {boolean} True if valid URL, false otherwise + * Validates if a string is a valid URL safe for interpolation into JS code. + * Rejects hash fragments (the exploit entry point), non-http(s) protocols, + * and characters that can break out of JS string literals — double quotes, + * single quotes, backticks (template literals), backslashes, and newlines. */ export const isValidURL = (url: string): boolean => { try { - new URL(url) + const parsed = new URL(url) + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false + if (parsed.hash) return false + if (/["'`\\\n\r\t]/.test(url)) return false return true } catch { return false diff --git a/packages/server/.env.example b/packages/server/.env.example index ad755d530c8..485b204da72 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -41,7 +41,7 @@ PORT=3000 # LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials # LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs -# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash,pg,mysql2,mongodb,ioredis,redis,typeorm,puppeteer,playwright,@zilliz/milvus2-sdk-node # ALLOW_BUILTIN_DEP=false