Skip to content

Commit 9cae08f

Browse files
Merge branch 'main' into stack-trace
2 parents be71986 + 1a6f914 commit 9cae08f

10 files changed

Lines changed: 992 additions & 189 deletions

File tree

packages/components/nodes/agentflow/Tool/Tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,11 @@ class Tool_Agentflow implements INode {
236236
// ex: \["a", "b", "c", "d", "e"\]
237237
let cleanedValue = value
238238
.replace(/\\"/g, '"') // \" -> "
239-
.replace(/\\\\/g, '\\') // \\ -> \
240239
.replace(/\\\[/g, '[') // \[ -> [
241240
.replace(/\\\]/g, ']') // \] -> ]
242241
.replace(/\\\{/g, '{') // \{ -> {
243242
.replace(/\\\}/g, '}') // \} -> }
243+
.replace(/\\\\/g, '\\') // \\ -> \ (unescape backslash last)
244244

245245
// Try to parse as JSON if it looks like JSON/array
246246
if (

packages/components/nodes/agents/CSVAgent/CSVAgent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ class CSV_Agents implements INode {
144144
// For example using titanic.csv: {'PassengerId': 'int64', 'Survived': 'int64', 'Pclass': 'int64', 'Name': 'object', 'Sex': 'object', 'Age': 'float64', 'SibSp': 'int64', 'Parch': 'int64', 'Ticket': 'object', 'Fare': 'float64', 'Cabin': 'object', 'Embarked': 'object'}
145145
let dataframeColDict = ''
146146
let customReadCSVFunc = _customReadCSV ? _customReadCSV : 'read_csv(csv_data)'
147+
const csvReadValidation = validatePythonCodeForDataFrame(customReadCSVFunc)
148+
if (!csvReadValidation.valid) {
149+
throw new Error(
150+
`Custom read_csv code was rejected for security reasons (${
151+
csvReadValidation.reason ?? 'unsafe construct'
152+
}). Please use only safe pandas read_csv operations.`
153+
)
154+
}
147155
try {
148156
const code = `import pandas as pd
149157
import base64

packages/components/nodes/tools/MCP/core.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,90 @@ export const validateEnvironmentVariables = (env: Record<string, any>): void =>
259259
}
260260
}
261261

262+
/**
263+
* Validates that command arguments don't contain flags that enable arbitrary code execution
264+
* This prevents attacks where whitelisted commands are used with dangerous flags
265+
* (e.g., "npx -c malicious-command" or "python -c malicious-code")
266+
* @param command The command to validate
267+
* @param args The arguments to validate
268+
*/
269+
export const validateCommandFlags = (command: string, args: string[]): void => {
270+
// Define dangerous flags for each command that enable code execution
271+
const dangerousFlagsByCommand: Record<string, string[]> = {
272+
npx: [
273+
'-c', // Execute shell commands
274+
'--call', // Execute shell commands
275+
'--shell-auto-fallback', // Shell execution fallback
276+
'-y' // Auto-confirms installation prompts
277+
],
278+
node: [
279+
'-e', // Execute JavaScript code
280+
'--eval', // Execute JavaScript code
281+
'-p', // Evaluate and print JavaScript code
282+
'--print', // Evaluate and print JavaScript code
283+
'--inspect', // Enable remote debugging (security risk)
284+
'--inspect-brk', // Enable remote debugging with breakpoint (security risk)
285+
'--experimental-policy' // Could load malicious policies
286+
],
287+
python: [
288+
'-c', // Execute Python code
289+
'-m' // Run library modules (could run malicious modules)
290+
],
291+
python3: [
292+
'-c', // Execute Python code
293+
'-m' // Run library modules (could run malicious modules)
294+
],
295+
docker: [
296+
'run', // Run containers (too powerful)
297+
'exec', // Execute in containers
298+
'-v', // Mount host filesystems
299+
'--volume', // Mount host filesystems
300+
'--privileged', // Privileged mode
301+
'--cap-add', // Add capabilities
302+
'--security-opt', // Modify security options
303+
'--network', // Host network access (catches --network=host and --network host)
304+
'--pid', // Host PID namespace (catches --pid=host and --pid host)
305+
'--ipc' // Host IPC namespace (catches --ipc=host and --ipc host)
306+
]
307+
}
308+
309+
const dangerousFlags = dangerousFlagsByCommand[command] || []
310+
311+
// Collect single-char dangerous flags (e.g. '-c' -> 'c') for combined flag detection
312+
const dangerousShortChars = new Set(dangerousFlags.filter((f) => /^-[a-zA-Z]$/.test(f)).map((f) => f[1].toLowerCase()))
313+
314+
for (const arg of args) {
315+
if (typeof arg !== 'string') continue
316+
317+
const normalizedArg = arg.toLowerCase().trim()
318+
319+
// Check for dangerous flags in various forms (exact, =value, space-separated value)
320+
for (const flag of dangerousFlags) {
321+
const lowerCaseFlag = flag.toLowerCase()
322+
if (normalizedArg === lowerCaseFlag) {
323+
throw new Error(`Argument '${arg}' is not allowed for command '${command}'.`)
324+
}
325+
if (normalizedArg.startsWith(lowerCaseFlag + '=')) {
326+
throw new Error(`Argument '${arg}' contains flag '${flag}' that is not allowed for command '${command}'.`)
327+
}
328+
if (flag.startsWith('-') && normalizedArg.startsWith(lowerCaseFlag + ' ')) {
329+
throw new Error(`Argument '${arg}' contains flag '${flag}' that is not allowed for command '${command}'.`)
330+
}
331+
}
332+
333+
// Check for combined short flags (e.g. "-yc" = "-y" + "-c")
334+
// A combined flag starts with a single '-', is not a long flag '--', and has multiple characters after '-'
335+
if (/^-[a-zA-Z]{2,}/.test(normalizedArg)) {
336+
const flagChars = normalizedArg.slice(1) // strip leading '-'
337+
for (const ch of flagChars) {
338+
if (dangerousShortChars.has(ch)) {
339+
throw new Error(`Argument '${arg}' contains dangerous flag '-${ch}' for command '${command}'.`)
340+
}
341+
}
342+
}
343+
}
344+
}
345+
262346
export const validateMCPServerConfig = (serverParams: any): void => {
263347
// Validate the entire server configuration
264348
if (!serverParams || typeof serverParams !== 'object') {
@@ -276,6 +360,11 @@ export const validateMCPServerConfig = (serverParams: any): void => {
276360
if (serverParams.args && Array.isArray(serverParams.args)) {
277361
validateArgsForLocalFileAccess(serverParams.args)
278362
validateCommandInjection(serverParams.args)
363+
364+
// Validate command-specific dangerous flags
365+
if (serverParams.command) {
366+
validateCommandFlags(serverParams.command, serverParams.args)
367+
}
279368
}
280369

281370
// Validate environment variables

packages/components/nodes/vectorstores/Faiss/Faiss.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FaissStore } from '@langchain/community/vectorstores/faiss'
44
import { Embeddings } from '@langchain/core/embeddings'
55
import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
66
import { getBaseClasses } from '../../../src/utils'
7+
import { validateVectorStorePath } from '../../../src/validator'
78

89
class Faiss_VectorStores implements INode {
910
label: string
@@ -88,7 +89,9 @@ class Faiss_VectorStores implements INode {
8889

8990
try {
9091
const vectorStore = await FaissStore.fromDocuments(finalDocs, embeddings)
91-
await vectorStore.save(basePath)
92+
// Validate and sanitize the base path to prevent path traversal attacks
93+
const validatedPath = validateVectorStorePath(basePath)
94+
await vectorStore.save(validatedPath)
9295

9396
// Avoid illegal invocation error
9497
vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number) => {
@@ -109,7 +112,9 @@ class Faiss_VectorStores implements INode {
109112
const topK = nodeData.inputs?.topK as string
110113
const k = topK ? parseFloat(topK) : 4
111114

112-
const vectorStore = await FaissStore.load(basePath, embeddings)
115+
// Validate and sanitize the base path to prevent path traversal attacks
116+
const validatedPath = validateVectorStorePath(basePath)
117+
const vectorStore = await FaissStore.load(validatedPath, embeddings)
113118

114119
// Avoid illegal invocation error
115120
vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number) => {

packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import path from 'path'
21
import { flatten } from 'lodash'
32
import { storageContextFromDefaults, serviceContextFromDefaults, VectorStoreIndex, Document } from 'llamaindex'
43
import { Document as LCDocument } from 'langchain/document'
54
import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
6-
import { getUserHome } from '../../../src'
5+
import { validateVectorStorePath } from '../../../src/validator'
76

87
class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode {
98
label: string
@@ -89,10 +88,6 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode {
8988
const embeddings = nodeData.inputs?.embeddings
9089
const model = nodeData.inputs?.model
9190

92-
let filePath = ''
93-
if (!basePath) filePath = path.join(getUserHome(), '.flowise', 'llamaindex')
94-
else filePath = basePath
95-
9691
const flattenDocs = docs && docs.length ? flatten(docs) : []
9792
const finalDocs = []
9893
for (let i = 0; i < flattenDocs.length; i += 1) {
@@ -105,6 +100,8 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode {
105100
}
106101

107102
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
103+
// Validate and sanitize the base path to prevent path traversal attacks
104+
const filePath = validateVectorStorePath(basePath)
108105
const storageContext = await storageContextFromDefaults({ persistDir: filePath })
109106

110107
try {
@@ -123,9 +120,8 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode {
123120
const topK = nodeData.inputs?.topK as string
124121
const k = topK ? parseFloat(topK) : 4
125122

126-
let filePath = ''
127-
if (!basePath) filePath = path.join(getUserHome(), '.flowise', 'llamaindex')
128-
else filePath = basePath
123+
// Validate and sanitize the base path to prevent path traversal attacks
124+
const filePath = validateVectorStorePath(basePath)
129125

130126
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
131127
const storageContext = await storageContextFromDefaults({ persistDir: filePath })

packages/components/src/httpSecurity.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ const DEFAULT_DENY_LIST = [
2727
]
2828

2929
/**
30-
* Gets the HTTP deny list from environment variable or returns default
31-
* @returns Array of denied IP addresses/CIDR ranges
30+
* Gets the HTTP deny list, always including default protections plus any custom entries
31+
* @returns Array of denied IP addresses/CIDR ranges (always includes DEFAULT_DENY_LIST)
3232
*/
3333
function getHttpDenyList(): string[] {
3434
const httpDenyListString = process.env.HTTP_DENY_LIST
35-
return httpDenyListString ? httpDenyListString.split(',').map((s) => s.trim()) : DEFAULT_DENY_LIST
35+
if (httpDenyListString) {
36+
const customList = httpDenyListString
37+
.split(',')
38+
.map((s) => s.trim())
39+
.filter(Boolean)
40+
return [...new Set([...DEFAULT_DENY_LIST, ...customList])]
41+
}
42+
return DEFAULT_DENY_LIST
3643
}
3744

3845
/**
@@ -97,7 +104,7 @@ export async function secureAxiosRequest(config: AxiosRequestConfig, maxRedirect
97104
}
98105

99106
let redirects = 0
100-
let currentConfig = { ...config, maxRedirects: 0 } // Disable automatic redirects
107+
let currentConfig = { ...config, maxRedirects: 0, validateStatus: () => true } // Disable automatic redirects, accept all status codes
101108

102109
while (redirects <= maxRedirects) {
103110
const target = await resolveAndValidate(currentUrl)

0 commit comments

Comments
 (0)