mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
Control Path Traversal Protections via Env Var (#5962)
* Fix isPathTraversal method to be less strict * Fix isPathTraversal method to be less strict * Control isPathTraversal via an Env Var * Path traversal check env var --------- Co-authored-by: christopherholland-workday <[email protected]>
This commit is contained in:
co-authored by
christopherholland-workday
parent
378d3ec3bd
commit
aff06479aa
@@ -190,6 +190,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||
|
||||
# HTTP_DENY_LIST=
|
||||
# HTTP_SECURITY_CHECK=true
|
||||
# PATH_TRAVERSAL_SAFETY=true
|
||||
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)
|
||||
|
||||
@@ -153,6 +153,7 @@ services:
|
||||
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
|
||||
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
|
||||
- TRUST_PROXY=${TRUST_PROXY}
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:${PORT:-3000}/api/v1/ping']
|
||||
@@ -300,6 +301,7 @@ services:
|
||||
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
|
||||
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
|
||||
- TRUST_PROXY=${TRUST_PROXY}
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:${WORKER_PORT:-5566}/healthz']
|
||||
|
||||
@@ -138,6 +138,7 @@ services:
|
||||
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
|
||||
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
|
||||
- TRUST_PROXY=${TRUST_PROXY}
|
||||
ports:
|
||||
- '${PORT}:${PORT}'
|
||||
|
||||
@@ -190,6 +190,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||
|
||||
# HTTP_DENY_LIST=
|
||||
# HTTP_SECURITY_CHECK=true
|
||||
# PATH_TRAVERSAL_SAFETY=true
|
||||
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)
|
||||
|
||||
@@ -138,6 +138,7 @@ services:
|
||||
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
|
||||
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
|
||||
- TRUST_PROXY=${TRUST_PROXY}
|
||||
ports:
|
||||
- '${WORKER_PORT}:${WORKER_PORT}'
|
||||
|
||||
@@ -33,17 +33,28 @@ export const isValidURL = (url: string): boolean => {
|
||||
* @returns {boolean} True if path traversal detected, false otherwise
|
||||
*/
|
||||
export const isPathTraversal = (path: string): boolean => {
|
||||
// Check for common path traversal patterns
|
||||
// PATH_TRAVERSAL_SAFETY defaults to true; must be explicitly set to 'false' to disable
|
||||
if (process.env.PATH_TRAVERSAL_SAFETY === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Normalize %2e → . before checking for .. to catch mixed-encoding bypasses
|
||||
// e.g. .%2e/, %2e./, %2e%2e all become ../
|
||||
if (/\.\./.test(path.replace(/%2e/gi, '.'))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const dangerousPatterns = [
|
||||
'..', // Directory traversal
|
||||
'/', // Root directory
|
||||
'\\', // Windows root directory
|
||||
'%2e', // URL encoded .
|
||||
'%2f', // URL encoded /
|
||||
'%5c' // URL encoded \
|
||||
/%2f/i, // URL encoded /
|
||||
/%5c/i, // URL encoded \ (Windows path)
|
||||
/\0/, // Null bytes
|
||||
/%00/i, // URL encoded null byte
|
||||
/^\s*[a-zA-Z]:[/\\]/, // Windows absolute paths (C:\, C:/) with optional leading whitespace
|
||||
/^\\\\[^\\]/, // UNC paths (\\server\)
|
||||
/^\// // Absolute Unix paths (/etc, /data, /root, etc.)
|
||||
]
|
||||
|
||||
return dangerousPatterns.some((pattern) => path.toLowerCase().includes(pattern))
|
||||
return dangerousPatterns.some((pattern) => pattern.test(path))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +63,10 @@ export const isPathTraversal = (path: string): boolean => {
|
||||
* @returns {boolean} True if path traversal detected, false otherwise
|
||||
*/
|
||||
export const isUnsafeFilePath = (filePath: string): boolean => {
|
||||
if (process.env.PATH_TRAVERSAL_SAFETY === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return true
|
||||
}
|
||||
@@ -191,6 +206,14 @@ const getAllowedVectorStoreBaseDirs = (): string[] => {
|
||||
* @throws {Error} If path validation fails or path is outside allowed directories
|
||||
*/
|
||||
export const validateVectorStorePath = (userProvidedPath: string | undefined): string => {
|
||||
if (process.env.PATH_TRAVERSAL_SAFETY === 'false') {
|
||||
if (!userProvidedPath || userProvidedPath.trim() === '') {
|
||||
return path.join(getUserHome(), '.flowise', 'vectorstore')
|
||||
}
|
||||
const bypassPath = userProvidedPath.trim()
|
||||
return path.isAbsolute(bypassPath) ? bypassPath : path.resolve(path.join(getUserHome(), '.flowise', bypassPath))
|
||||
}
|
||||
|
||||
// If no path provided, use default secure location
|
||||
if (!userProvidedPath || userProvidedPath.trim() === '') {
|
||||
return path.join(getUserHome(), '.flowise', 'vectorstore')
|
||||
|
||||
@@ -1,7 +1,86 @@
|
||||
import { validateMimeTypeAndExtensionMatch, validateVectorStorePath } from '../src/validator'
|
||||
import { isPathTraversal, isUnsafeFilePath, validateMimeTypeAndExtensionMatch, validateVectorStorePath } from '../src/validator'
|
||||
import path from 'path'
|
||||
import { getUserHome } from '../src/utils'
|
||||
|
||||
describe('isPathTraversal', () => {
|
||||
describe('returns true for dangerous patterns', () => {
|
||||
it.each([
|
||||
['directory traversal (..)', '../etc/passwd'],
|
||||
['multiple levels of traversal', '../../sensitive'],
|
||||
['bare double-dot', '..'],
|
||||
['Windows absolute path', 'C:\\windows'],
|
||||
['Windows absolute path with forward slash', 'C:/windows'],
|
||||
['Windows absolute path with leading whitespace', ' C:\\windows'],
|
||||
['UNC path', '\\\\server\\share'],
|
||||
['URL encoded dot (%2e)', '%2e%2e/etc'],
|
||||
['URL encoded dot uppercase (%2E)', '%2E%2E'],
|
||||
['mixed encoding (.%2e)', '.%2e/etc'],
|
||||
['mixed encoding (%2e.)', '%2e./etc'],
|
||||
['URL encoded forward slash (%2f)', '%2f'],
|
||||
['URL encoded forward slash uppercase (%2F)', '%2F'],
|
||||
['URL encoded backslash (%5c)', '%5c'],
|
||||
['URL encoded backslash uppercase (%5C)', '%5C'],
|
||||
['null byte', 'path\0name'],
|
||||
['URL encoded null byte (%00)', 'path%00name'],
|
||||
['absolute Unix path', '/etc/passwd'],
|
||||
['absolute Unix root', '/']
|
||||
])('should detect %s: %s', (_description, input) => {
|
||||
expect(isPathTraversal(input)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('returns false for safe inputs', () => {
|
||||
it.each([
|
||||
['simple filename with extension', 'filename.txt'],
|
||||
['plain name without extension', 'myfile'],
|
||||
['empty string', ''],
|
||||
['name with underscores', 'hello_world'],
|
||||
['relative path with slash', 'uploads/file.txt']
|
||||
])('should not flag %s: %s', (_description, input) => {
|
||||
expect(isPathTraversal(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATH_TRAVERSAL_SAFETY=false bypasses all checks', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PATH_TRAVERSAL_SAFETY = 'false'
|
||||
})
|
||||
afterEach(() => {
|
||||
delete process.env.PATH_TRAVERSAL_SAFETY
|
||||
})
|
||||
|
||||
it.each([
|
||||
['absolute Unix path', '/data/uploads'],
|
||||
['mixed encoding', '.%2e/etc'],
|
||||
['directory traversal', '../etc/passwd'],
|
||||
['Windows absolute path', 'C:\\windows']
|
||||
])('should return false for %s when safety disabled', (_desc, input) => {
|
||||
expect(isPathTraversal(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUnsafeFilePath', () => {
|
||||
describe('PATH_TRAVERSAL_SAFETY=false bypasses all checks', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PATH_TRAVERSAL_SAFETY = 'false'
|
||||
})
|
||||
afterEach(() => {
|
||||
delete process.env.PATH_TRAVERSAL_SAFETY
|
||||
})
|
||||
|
||||
it.each([
|
||||
['absolute Unix path', '/data/uploads'],
|
||||
['directory traversal', '../etc/passwd'],
|
||||
['Windows absolute path', 'C:\\windows'],
|
||||
['null byte', 'path\0name'],
|
||||
['control character', 'path\x01name']
|
||||
])('should return false for %s when safety disabled', (_desc, input) => {
|
||||
expect(isUnsafeFilePath(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateMimeTypeAndExtensionMatch', () => {
|
||||
describe('valid cases', () => {
|
||||
it.each([
|
||||
@@ -359,4 +438,31 @@ describe('validateVectorStorePath', () => {
|
||||
expect(result).toBe(path.normalize(mixedPath))
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATH_TRAVERSAL_SAFETY=false bypasses all checks', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PATH_TRAVERSAL_SAFETY = 'false'
|
||||
})
|
||||
afterEach(() => {
|
||||
delete process.env.PATH_TRAVERSAL_SAFETY
|
||||
})
|
||||
|
||||
it('should allow arbitrary absolute Unix path', () => {
|
||||
expect(validateVectorStorePath('/data/faiss-store')).toBe('/data/faiss-store')
|
||||
})
|
||||
|
||||
it('should allow path outside allowed directories (/tmp)', () => {
|
||||
expect(validateVectorStorePath('/tmp/mystore')).toBe('/tmp/mystore')
|
||||
})
|
||||
|
||||
it('should allow path containing .. without throwing', () => {
|
||||
const result = validateVectorStorePath('../mystore')
|
||||
expect(typeof result).toBe('string')
|
||||
})
|
||||
|
||||
it('should return default path when undefined', () => {
|
||||
const userHome = getUserHome()
|
||||
expect(validateVectorStorePath(undefined)).toBe(path.join(userHome, '.flowise', 'vectorstore'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -189,6 +189,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||
|
||||
# HTTP_DENY_LIST=
|
||||
# HTTP_SECURITY_CHECK=true
|
||||
# PATH_TRAVERSAL_SAFETY=true
|
||||
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)
|
||||
|
||||
@@ -106,6 +106,7 @@ export abstract class BaseCommand extends Command {
|
||||
CUSTOM_MCP_PROTOCOL: Flags.string(),
|
||||
HTTP_DENY_LIST: Flags.string(),
|
||||
HTTP_SECURITY_CHECK: Flags.string(),
|
||||
PATH_TRAVERSAL_SAFETY: Flags.string(),
|
||||
TRUST_PROXY: Flags.string(),
|
||||
|
||||
// Auth
|
||||
|
||||
Reference in New Issue
Block a user