feat(security): enhance file path validation and implement non-root D… (#5474)

* feat(security): enhance file path validation and implement non-root Docker user

- Validate resolved full file paths including workspace boundaries in SecureFileStore
- Resolve paths before validation in readFile and writeFile operations
- Run Docker container as non-root flowise user (uid/gid 1001)
- Apply proper file ownership and permissions for application files

Prevents path traversal attacks and follows container security best practices

* Add sensitive system directory validation and Flowise internal file protection

* Update Dockerfile to use default node user

* update validation patterns to include additional system binary directories (/usr/bin, /usr/sbin, /usr/local/bin)

* added isSafeBrowserExecutable function to validate browser executable paths for Playwright and Puppeteer loaders

---------

Co-authored-by: taraka-vishnumolakala <taraka.vishnumolakala@workday.com>
Co-authored-by: Henry Heng <henryheng@flowiseai.com>
Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Taraka Vishnumolakala
2025-11-15 10:03:01 -05:00
committed by GitHub
parent 4a642f02d0
commit 2414057c08
5 changed files with 189 additions and 29 deletions
+113
View File
@@ -70,6 +70,35 @@ export const isUnsafeFilePath = (filePath: string): boolean => {
return dangerousPatterns.some((pattern) => pattern.test(filePath))
}
/**
* Validates if a resolved path accesses sensitive system directories
* Uses pattern-based detection to identify known sensitive system directories
* at root level or one level deep, while allowing legitimate paths like /usr/src
* @param {string} resolvedPath The resolved absolute path to validate
* @returns {boolean} True if path accesses sensitive system directory, false otherwise
*/
export const isSensitiveSystemPath = (resolvedPath: string): boolean => {
if (!resolvedPath || typeof resolvedPath !== 'string') {
return false
}
// Pattern-based detection for known sensitive system directories:
// Blocks obvious system directories while allowing legitimate paths like /usr/src, /usr/local/src, /opt, etc.
// 1. At root level (e.g., /etc, /sys, /bin, /sbin) - one segment after root
// 2. One level deep (e.g., /etc/passwd, /sys/kernel, /var/log) - two segments total
// 3. Specific sensitive subdirectories (e.g., /var/log, /var/run) - two segments with specific parent
// 4. System binary directories (e.g., /usr/bin, /usr/sbin, /usr/local/bin) - prevents overwriting system executables
const sensitiveSystemPatterns = [
/^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)([/\\]|$)/i, // Root level: /etc, /sys, /proc, /bin, /sbin, etc.
/^[/\\](etc|sys|proc|dev|boot|root|bin|sbin)[/\\][^/\\]*$/i, // One level deep: /etc/passwd, /sys/kernel, /bin/sh, etc.
/^[/\\]var[/\\](log|run|lib|spool|mail)([/\\]|$)/i, // Sensitive /var subdirectories: /var/log, /var/run, etc.
/^[/\\]usr[/\\](bin|sbin)([/\\]|$)/i, // System binary directories: /usr/bin, /usr/sbin
/^[/\\]usr[/\\]local[/\\](bin|sbin)([/\\]|$)/i // Local system binaries: /usr/local/bin, /usr/local/sbin
]
return sensitiveSystemPatterns.some((pattern) => pattern.test(resolvedPath))
}
/**
* Validates if a file path is within the allowed workspace boundaries
* @param {string} filePath The file path to validate
@@ -102,3 +131,87 @@ export const isWithinWorkspace = (filePath: string, workspacePath: string): bool
return false
}
}
/**
* Validates if a browser executable path is safe to use
* Prevents arbitrary code execution through environment variable manipulation
* @param {string} executablePath The browser executable path to validate
* @returns {boolean} True if path is safe, false otherwise
*/
export const isSafeBrowserExecutable = (executablePath: string | undefined): boolean => {
if (!executablePath) {
return true // If not specified, let browser library use its default
}
if (typeof executablePath !== 'string' || executablePath.trim() === '') {
return false
}
const path = require('path')
const fs = require('fs')
try {
// Normalize the path
const normalizedPath = path.normalize(executablePath)
// Must be an absolute path
if (!path.isAbsolute(normalizedPath)) {
return false
}
// Allowed browser executable locations (system-managed only)
const allowedPaths = [
// Linux/Unix Chromium/Chrome paths
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chrome',
'/snap/bin/chromium',
// macOS Chrome/Chromium paths
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
// Windows Chrome/Chromium paths (normalized with forward slashes)
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
'C:/Program Files/Chromium/Application/chrome.exe',
// Firefox paths
'/usr/bin/firefox',
'/Applications/Firefox.app/Contents/MacOS/firefox',
'C:/Program Files/Mozilla Firefox/firefox.exe',
'C:/Program Files (x86)/Mozilla Firefox/firefox.exe'
]
// Normalize allowed paths for comparison (handle Windows backslashes)
const normalizedAllowedPaths = allowedPaths.map((p) => path.normalize(p))
// Check if the path exactly matches one of the allowed paths
const isAllowedPath = normalizedAllowedPaths.some((allowedPath) => normalizedPath.toLowerCase() === allowedPath.toLowerCase())
if (!isAllowedPath) {
return false
}
// Additional security: Verify file exists and is executable (where applicable)
// This prevents using a path before malicious file is written
try {
if (fs.existsSync(normalizedPath)) {
const stats = fs.statSync(normalizedPath)
// On Unix-like systems, check if file is executable
if (process.platform !== 'win32') {
// Check if file has execute permissions (using bitwise AND)
// 0o111 checks for execute permission for user, group, or others
return (stats.mode & 0o111) !== 0
}
return stats.isFile()
}
// If file doesn't exist, reject it (prevents race conditions)
return false
} catch {
return false
}
} catch (error) {
// If any error occurs during validation, deny access
return false
}
}