Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion docker/worker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, string> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand Down
60 changes: 59 additions & 1 deletion packages/components/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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)
})
15 changes: 4 additions & 11 deletions packages/components/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export const availableDependencies = [
'@qdrant/js-client-rest',
'@supabase/supabase-js',
'@upstash/redis',
'@zilliz/milvus2-sdk-node',
'apify-client',
'cheerio',
'chromadb',
Expand All @@ -97,32 +96,24 @@ export const availableDependencies = [
'google-auth-library',
'graphql',
'html-to-text',
'ioredis',
'langchain',
'langfuse',
'langsmith',
'langwatch',
'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']

Expand Down Expand Up @@ -1780,6 +1771,7 @@ export const executeJavaScriptCode = async (
},
eval: false,
wasm: false,
fixAsync: true,
timeout: timeoutMs
}

Expand All @@ -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)
Expand Down
60 changes: 59 additions & 1 deletion packages/components/src/validator.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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,<script>alert(1)</script>']
])('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)
})
})
})
12 changes: 8 additions & 4 deletions packages/components/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading