Skip to content

Commit 3f257bd

Browse files
authored
fix: FLOWISE-400, 543, 551 (#6417)
* fix: available dependencies in sandbox * fix: better url validation and prevent arbitrary url injection into the sandbox * fix: permissions in docker * revert: all docker-related changes * fix: review feedback
1 parent 96a9b23 commit 3f257bd

11 files changed

Lines changed: 148 additions & 29 deletions

File tree

docker/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ PORT=3000
4141
# 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
4242
# LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie
4343
# TOOL_FUNCTION_BUILTIN_DEP=crypto,fs
44-
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash
44+
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash,pg,mysql2,mongodb,ioredis,redis,typeorm,puppeteer,playwright,@zilliz/milvus2-sdk-node
4545
# ALLOW_BUILTIN_DEP=false
4646

4747

docker/worker/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ WORKER_PORT=5566
4141
# 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
4242
# LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie
4343
# TOOL_FUNCTION_BUILTIN_DEP=crypto,fs
44-
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash
44+
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash,pg,mysql2,mongodb,ioredis,redis,typeorm,puppeteer,playwright,@zilliz/milvus2-sdk-node
4545
# ALLOW_BUILTIN_DEP=false
4646

4747

packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { AxiosRequestConfig } from 'axios'
1111
import { secureAxiosRequest } from '../../../src/httpSecurity'
1212
import { getCredentialData, getCredentialParam, processTemplateVariables, parseJsonBody } from '../../../src/utils'
13+
import { isValidURL } from '../../../src/validator'
1314
import { DataSource } from 'typeorm'
1415
import { BaseMessageLike } from '@langchain/core/messages'
1516
import { updateFlowState } from '../utils'
@@ -183,6 +184,8 @@ class ExecuteFlow_Agentflow implements INode {
183184
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
184185
const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData)
185186

187+
if (!baseURL || !isValidURL(baseURL)) throw new Error('Invalid base URL: must be a valid URL')
188+
186189
if (selectedFlowId === options.chatflowid) throw new Error('Cannot call the same agentflow!')
187190

188191
let headers: Record<string, string> = {

packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,19 +239,20 @@ class ExecuteFlow_SeqAgents implements INode {
239239
// Create additional sandbox variables
240240
const additionalSandbox: ICommonObject = {
241241
$callOptions: callOptions,
242-
$callBody: body
242+
$callBody: body,
243+
$apiURL: `${baseURL}/api/v1/prediction/${selectedFlowId}`
243244
}
244245

245246
const sandbox = createCodeExecutionSandbox(flowInput, variables, flow, additionalSandbox)
246247

247248
const code = `
248249
const fetch = require('node-fetch');
249-
const url = "${baseURL}/api/v1/prediction/${selectedFlowId}";
250-
250+
const url = $apiURL;
251+
251252
const body = $callBody;
252-
253+
253254
const options = $callOptions;
254-
255+
255256
try {
256257
const response = await fetch(url, options);
257258
const resp = await response.json();

packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ class AgentflowTool extends StructuredTool {
345345

346346
const code = `
347347
const fetch = require('node-fetch');
348-
const url = "${this.baseURL}/api/v1/prediction/${this.agentflowid}";
348+
const url = $apiURL;
349349
350350
const body = $callBody;
351351
@@ -364,7 +364,8 @@ try {
364364
// Create additional sandbox variables
365365
const additionalSandbox: ICommonObject = {
366366
$callOptions: options,
367-
$callBody: body
367+
$callBody: body,
368+
$apiURL: `${this.baseURL}/api/v1/prediction/${this.agentflowid}`
368369
}
369370

370371
const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox)

packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ class ChatflowTool extends StructuredTool {
353353

354354
const code = `
355355
const fetch = require('node-fetch');
356-
const url = "${this.baseURL}/api/v1/prediction/${this.chatflowid}";
356+
const url = $apiURL;
357357
358358
const body = $callBody;
359359
@@ -372,7 +372,8 @@ try {
372372
// Create additional sandbox variables
373373
const additionalSandbox: ICommonObject = {
374374
$callOptions: options,
375-
$callBody: body
375+
$callBody: body,
376+
$apiURL: `${this.baseURL}/api/v1/prediction/${this.chatflowid}`
376377
}
377378

378379
const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox)

packages/components/src/utils.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { removeInvalidImageMarkdown, convertRequireToImport, COMMONJS_REQUIRE_REGEX, IMPORT_EXTRACTION_REGEX } from './utils'
1+
import {
2+
removeInvalidImageMarkdown,
3+
convertRequireToImport,
4+
COMMONJS_REQUIRE_REGEX,
5+
IMPORT_EXTRACTION_REGEX,
6+
executeJavaScriptCode
7+
} from './utils'
28

39
describe('removeInvalidImageMarkdown', () => {
410
describe('strips non-http/https image markdown', () => {
@@ -229,3 +235,55 @@ describe('Import extraction regex (utils.ts line 1596 pattern)', () => {
229235
expect(extractModules('console.log("hello")')).toEqual([])
230236
})
231237
})
238+
239+
// ---------------------------------------------------------------------------
240+
// NodeVM sandbox — availableDependencies allowlist
241+
// ---------------------------------------------------------------------------
242+
243+
describe('NodeVM sandbox — availableDependencies allowlist', () => {
244+
afterEach(() => {
245+
delete process.env.ALLOW_BUILTIN_DEP
246+
delete process.env.TOOL_FUNCTION_EXTERNAL_DEP
247+
})
248+
249+
describe('high-risk packages are blocked even when ALLOW_BUILTIN_DEP=true', () => {
250+
beforeEach(() => {
251+
process.env.ALLOW_BUILTIN_DEP = 'true'
252+
})
253+
254+
const removedPackages = [
255+
'pg',
256+
'mysql2',
257+
'mongodb',
258+
'ioredis',
259+
'redis',
260+
'typeorm',
261+
'puppeteer',
262+
'playwright',
263+
'@zilliz/milvus2-sdk-node'
264+
]
265+
266+
test.each(removedPackages)(
267+
"require('%s') is denied",
268+
async (pkg) => {
269+
await expect(
270+
executeJavaScriptCode(`const m = require('${pkg}'); return 'loaded'`, {}, { timeout: 10000 })
271+
).rejects.toThrow()
272+
},
273+
15000
274+
)
275+
})
276+
277+
it('packages remaining in availableDependencies are still accessible with ALLOW_BUILTIN_DEP=true', async () => {
278+
process.env.ALLOW_BUILTIN_DEP = 'true'
279+
const result = await executeJavaScriptCode(`const cheerio = require('cheerio'); return typeof cheerio.load`, {}, { timeout: 10000 })
280+
expect(result).toBe('function')
281+
}, 15000)
282+
283+
it('a removed package becomes accessible via TOOL_FUNCTION_EXTERNAL_DEP', async () => {
284+
process.env.ALLOW_BUILTIN_DEP = 'true'
285+
process.env.TOOL_FUNCTION_EXTERNAL_DEP = 'pg'
286+
const result = await executeJavaScriptCode(`const { Client } = require('pg'); return typeof Client`, {}, { timeout: 10000 })
287+
expect(result).toBe('function')
288+
}, 15000)
289+
})

packages/components/src/utils.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const availableDependencies = [
8686
'@qdrant/js-client-rest',
8787
'@supabase/supabase-js',
8888
'@upstash/redis',
89-
'@zilliz/milvus2-sdk-node',
9089
'apify-client',
9190
'cheerio',
9291
'chromadb',
@@ -97,32 +96,24 @@ export const availableDependencies = [
9796
'google-auth-library',
9897
'graphql',
9998
'html-to-text',
100-
'ioredis',
10199
'langchain',
102100
'langfuse',
103101
'langsmith',
104102
'langwatch',
105103
'linkifyjs',
106104
'lunary',
107105
'mammoth',
108-
'mongodb',
109-
'mysql2',
110106
'node-html-markdown',
111107
'notion-to-md',
112108
'openai',
113109
'pdf-parse',
114110
'pdfjs-dist',
115-
'pg',
116-
'playwright',
117-
'puppeteer',
118-
'redis',
119111
'replicate',
120112
'srt-parser-2',
121-
'typeorm',
122113
'weaviate-client'
123114
]
124115

125-
const defaultAllowExternalDependencies = ['axios', 'moment', 'node-fetch']
116+
const defaultAllowExternalDependencies = ['axios', 'node-fetch']
126117

127118
export const defaultAllowBuiltInDep = ['assert', 'buffer', 'crypto', 'events', 'path', 'querystring', 'timers', 'url', 'zlib']
128119

@@ -1780,6 +1771,7 @@ export const executeJavaScriptCode = async (
17801771
},
17811772
eval: false,
17821773
wasm: false,
1774+
fixAsync: true,
17831775
timeout: timeoutMs
17841776
}
17851777

@@ -1789,7 +1781,8 @@ export const executeJavaScriptCode = async (
17891781
...nodeVMOptions,
17901782
require: defaultNodeVMOptions.require,
17911783
eval: false,
1792-
wasm: false
1784+
wasm: false,
1785+
fixAsync: true
17931786
}
17941787

17951788
const vm = new NodeVM(finalNodeVMOptions)

packages/components/src/validator.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPathTraversal, isUnsafeFilePath, validateMimeTypeAndExtensionMatch, validateVectorStorePath } from './validator'
1+
import { isPathTraversal, isUnsafeFilePath, isValidURL, validateMimeTypeAndExtensionMatch, validateVectorStorePath } from './validator'
22
import path from 'path'
33
import { getUserHome } from './utils'
44

@@ -466,3 +466,61 @@ describe('validateVectorStorePath', () => {
466466
})
467467
})
468468
})
469+
470+
describe('isValidURL', () => {
471+
describe('accepts valid http/https URLs', () => {
472+
it.each([
473+
['bare http host', 'http://localhost:3000'],
474+
['https with path', 'https://flowise.example.com/api'],
475+
['http with port and path', 'http://192.168.1.1:3000/api/v1'],
476+
['https with query string', 'https://example.com/search?q=hello']
477+
])('should accept %s', (_desc, url) => {
478+
expect(isValidURL(url)).toBe(true)
479+
})
480+
})
481+
482+
describe('rejects non-http(s) protocols', () => {
483+
it.each([
484+
['file protocol', 'file:///etc/passwd'],
485+
['javascript protocol', 'javascript:alert(1)'],
486+
['ftp protocol', 'ftp://example.com'],
487+
['data URI', 'data:text/html,<script>alert(1)</script>']
488+
])('should reject %s', (_desc, url) => {
489+
expect(isValidURL(url)).toBe(false)
490+
})
491+
})
492+
493+
describe('rejects URLs with hash fragments (CVE-2022-24785 bypass entry point)', () => {
494+
it.each([
495+
['plain hash', 'http://localhost:3000/#section'],
496+
['hash with injection payload', 'https://evil.com/#";\nrequire("child_process").exec("id");//'],
497+
['hash with quote escape', 'http://localhost:3000/#";malicious;//']
498+
])('should reject %s', (_desc, url) => {
499+
expect(isValidURL(url)).toBe(false)
500+
})
501+
})
502+
503+
describe('rejects URLs containing JS string-breaking characters', () => {
504+
it.each([
505+
['double quote', 'http://localhost:3000/path"suffix'],
506+
['single quote', "http://localhost:3000/path'suffix"],
507+
['backtick', 'http://localhost:3000/path`suffix'],
508+
['backslash', 'http://localhost:3000/path\\suffix'],
509+
['newline', 'http://localhost:3000/path\nsuffix'],
510+
['carriage return', 'http://localhost:3000/path\rsuffix'],
511+
['tab', 'http://localhost:3000/path\tsuffix']
512+
])('should reject URL with %s', (_desc, url) => {
513+
expect(isValidURL(url)).toBe(false)
514+
})
515+
})
516+
517+
describe('rejects malformed or empty inputs', () => {
518+
it.each([
519+
['empty string', ''],
520+
['not a URL', 'not-a-url'],
521+
['relative path', '/api/v1/prediction/abc']
522+
])('should reject %s', (_desc, url) => {
523+
expect(isValidURL(url)).toBe(false)
524+
})
525+
})
526+
})

packages/components/src/validator.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ export const isValidUUID = (uuid: string): boolean => {
1414
}
1515

1616
/**
17-
* Validates if a string is a valid URL
18-
* @param {string} url The string to validate
19-
* @returns {boolean} True if valid URL, false otherwise
17+
* Validates if a string is a valid URL safe for interpolation into JS code.
18+
* Rejects hash fragments (the exploit entry point), non-http(s) protocols,
19+
* and characters that can break out of JS string literals — double quotes,
20+
* single quotes, backticks (template literals), backslashes, and newlines.
2021
*/
2122
export const isValidURL = (url: string): boolean => {
2223
try {
23-
new URL(url)
24+
const parsed = new URL(url)
25+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
26+
if (parsed.hash) return false
27+
if (/["'`\\\n\r\t]/.test(url)) return false
2428
return true
2529
} catch {
2630
return false

0 commit comments

Comments
 (0)