Skip to content

Commit 999e088

Browse files
committed
fix: better url validation and prevent arbitrary url injection into the sandbox
1 parent 3941be6 commit 999e088

6 files changed

Lines changed: 80 additions & 14 deletions

File tree

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/validator.test.ts

Lines changed: 58 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,60 @@ 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+
['backslash', 'http://localhost:3000/path\\suffix'],
508+
['newline', 'http://localhost:3000/path\nsuffix'],
509+
['carriage return', 'http://localhost:3000/path\rsuffix'],
510+
['tab', 'http://localhost:3000/path\tsuffix']
511+
])('should reject URL with %s', (_desc, url) => {
512+
expect(isValidURL(url)).toBe(false)
513+
})
514+
})
515+
516+
describe('rejects malformed or empty inputs', () => {
517+
it.each([
518+
['empty string', ''],
519+
['not a URL', 'not-a-url'],
520+
['relative path', '/api/v1/prediction/abc']
521+
])('should reject %s', (_desc, url) => {
522+
expect(isValidURL(url)).toBe(false)
523+
})
524+
})
525+
})

packages/components/src/validator.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ 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 a JS double-quoted string literal.
2020
*/
2121
export const isValidURL = (url: string): boolean => {
2222
try {
23-
new URL(url)
23+
const parsed = new URL(url)
24+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
25+
if (parsed.hash) return false
26+
if (/["'\\\n\r\t]/.test(url)) return false
2427
return true
2528
} catch {
2629
return false

0 commit comments

Comments
 (0)