diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index 8a40d7ea2..f05074593 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -10,7 +10,6 @@ import { test } from 'linkifyjs' import { omit } from 'lodash' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { isSafeBrowserExecutable } from '../../../src/validator' class Playwright_DocumentLoaders implements INode { label: string @@ -193,9 +192,6 @@ class Playwright_DocumentLoaders implements INode { let docs = [] const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH - if (!isSafeBrowserExecutable(executablePath)) { - throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) - } const config: PlaywrightWebBaseLoaderOptions = { launchOptions: { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 0e5bdacb8..9b4ada916 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -6,7 +6,6 @@ import { omit } from 'lodash' import { PuppeteerLifeCycleEvent } from 'puppeteer' import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { isSafeBrowserExecutable } from '../../../src/validator' class Puppeteer_DocumentLoaders implements INode { label: string @@ -184,9 +183,6 @@ class Puppeteer_DocumentLoaders implements INode { let docs: Document[] = [] const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH - if (!isSafeBrowserExecutable(executablePath)) { - throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `) - } const config: PuppeteerWebBaseLoaderOptions = { launchOptions: { diff --git a/packages/components/nodes/tools/ReadFile/ReadFile.ts b/packages/components/nodes/tools/ReadFile/ReadFile.ts deleted file mode 100644 index eb703a1de..000000000 --- a/packages/components/nodes/tools/ReadFile/ReadFile.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class ReadFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Read File' - this.name = 'readFile' - this.version = 2.0 - this.type = 'ReadFile' - this.icon = 'readfile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.' - this.description = 'Read file from disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be read', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new ReadFileTool({ store }) - } -} - -interface ReadFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for reading files from the disk. Extends the StructuredTool - * class. - */ -export class ReadFileTool extends StructuredTool { - static lc_name() { - return 'ReadFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file') - }) as any - - name = 'read_file' - - description = 'Read file from disk' - - store: BaseFileStore - - constructor({ store }: ReadFileParams) { - super(...arguments) - - this.store = store - } - - async _call({ file_path }: z.infer) { - return await this.store.readFile(file_path) - } -} - -module.exports = { nodeClass: ReadFile_Tools } diff --git a/packages/components/nodes/tools/ReadFile/readfile.svg b/packages/components/nodes/tools/ReadFile/readfile.svg deleted file mode 100644 index c7cba0efa..000000000 --- a/packages/components/nodes/tools/ReadFile/readfile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/nodes/tools/WriteFile/WriteFile.ts b/packages/components/nodes/tools/WriteFile/WriteFile.ts deleted file mode 100644 index bc3609beb..000000000 --- a/packages/components/nodes/tools/WriteFile/WriteFile.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class WriteFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Write File' - this.name = 'writeFile' - this.version = 2.0 - this.type = 'WriteFile' - this.icon = 'writefile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.' - this.description = 'Write file to disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be written', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new WriteFileTool({ store }) - } -} - -interface WriteFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for writing data to files on the disk. Extends the StructuredTool - * class. - */ -export class WriteFileTool extends StructuredTool { - static lc_name() { - return 'WriteFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file'), - text: z.string().describe('text to write to file') - }) as any - - name = 'write_file' - - description = 'Write file to disk' - - store: BaseFileStore - - constructor({ store, ...rest }: WriteFileParams) { - super(rest) - - this.store = store - } - - async _call({ file_path, text }: z.infer) { - await this.store.writeFile(file_path, text) - return `File written to ${file_path} successfully.` - } -} - -module.exports = { nodeClass: WriteFile_Tools } diff --git a/packages/components/nodes/tools/WriteFile/writefile.svg b/packages/components/nodes/tools/WriteFile/writefile.svg deleted file mode 100644 index 0df04ea44..000000000 --- a/packages/components/nodes/tools/WriteFile/writefile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/src/SecureFileStore.ts b/packages/components/src/SecureFileStore.ts deleted file mode 100644 index fc50d7732..000000000 --- a/packages/components/src/SecureFileStore.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Serializable } from '@langchain/core/load/serializable' -import * as fs from 'fs' -import { NodeFileStore } from 'langchain/stores/file/node' -import * as path from 'path' -import { isSensitiveSystemPath, isUnsafeFilePath, isWithinWorkspace } from './validator' - -/** - * Security configuration for file operations - */ -export interface FileSecurityConfig { - /** Base workspace path - all file operations are restricted to this directory */ - workspacePath: string - /** Whether to enforce workspace boundaries (default: true) */ - enforceWorkspaceBoundaries?: boolean - /** Maximum file size in bytes (default: 10MB) */ - maxFileSize?: number - /** Allowed file extensions (if empty, all extensions allowed) */ - allowedExtensions?: string[] - /** Blocked file extensions */ - blockedExtensions?: string[] -} - -/** - * Secure file store that enforces workspace boundaries and validates file operations - */ -export class SecureFileStore extends Serializable { - lc_namespace = ['flowise', 'components', 'stores', 'file'] - - private config: Required - private nodeFileStore: NodeFileStore - - constructor(config: FileSecurityConfig) { - super() - - // Set default configuration - this.config = { - workspacePath: config.workspacePath, - enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true, - maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default - allowedExtensions: config.allowedExtensions ?? [], - blockedExtensions: config.blockedExtensions ?? [ - '.exe', - '.bat', - '.cmd', - '.sh', - '.ps1', - '.vbs', - '.scr', - '.com', - '.pif', - '.dll', - '.sys', - '.msi', - '.jar' - ] - } - - // Validate workspace path - if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) { - throw new Error('Workspace path must be an absolute path') - } - - // Ensure workspace directory exists - if (!fs.existsSync(this.config.workspacePath)) { - throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`) - } - - // Validate that workspace path is not a sensitive system directory - // This prevents setting workspace to /usr/bin, /etc, etc. which would allow access to system files - if (isSensitiveSystemPath(path.normalize(this.config.workspacePath))) { - throw new Error(`Workspace path cannot be set to sensitive system directory: ${this.config.workspacePath}`) - } - - // Initialize the underlying NodeFileStore with workspace path - this.nodeFileStore = new NodeFileStore(this.config.workspacePath) - } - - /** - * Validates a file path against security policies - * @param filePath The raw user-provided file path (relative to workspace) - * @param resolvedPath The resolved absolute path (for extension validation) - */ - private validateFilePath(filePath: string, resolvedPath: string): void { - // Validate the raw user input for unsafe patterns (path traversal, absolute paths, etc.) - // This must be done on the raw input, not the resolved path, because isUnsafeFilePath - // is designed to detect absolute paths in user input - if (isUnsafeFilePath(filePath)) { - throw new Error(`Unsafe file path detected: ${filePath}`) - } - - // Enforce workspace boundaries if enabled (this handles path resolution internally) - if (this.config.enforceWorkspaceBoundaries) { - if (!isWithinWorkspace(filePath, this.config.workspacePath)) { - throw new Error(`File path outside workspace boundaries: ${filePath}`) - } - } - - // Prevent access to Flowise internal files (any path containing .flowise) - const normalizedResolved = path.normalize(resolvedPath) - if (normalizedResolved.includes('.flowise')) { - throw new Error(`Access to Flowise internal files denied: ${filePath}`) - } - - // Validate that the resolved path does not access sensitive system directories - // This prevents access to system files even if workspace is set to a system directory - if (isSensitiveSystemPath(normalizedResolved)) { - throw new Error(`Access to sensitive system directory denied: ${filePath}`) - } - - // Check file extension on the resolved path to get the actual extension - const ext = path.extname(resolvedPath).toLowerCase() - - // Check blocked extensions - if (this.config.blockedExtensions.includes(ext)) { - throw new Error(`File extension not allowed: ${ext}`) - } - - // Check allowed extensions (if specified) - if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) { - throw new Error(`File extension not in allowed list: ${ext}`) - } - } - - /** - * Validates file size - */ - private validateFileSize(content: string): void { - const sizeInBytes = Buffer.byteLength(content, 'utf8') - if (sizeInBytes > this.config.maxFileSize) { - throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`) - } - } - - /** - * Reads a file with security validation - */ - async readFile(filePath: string): Promise { - // Resolve the full path for extension validation - const resolvedPath = path.resolve(this.config.workspacePath, filePath) - // Validate the raw user input (not the resolved path) to avoid false positives - this.validateFilePath(filePath, resolvedPath) - - try { - return await this.nodeFileStore.readFile(filePath) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to read file: ${path.basename(filePath)}`) - } - } - - /** - * Writes a file with security validation - */ - async writeFile(filePath: string, contents: string): Promise { - this.validateFileSize(contents) - - // Resolve the full path for extension validation and directory creation - const resolvedPath = path.resolve(this.config.workspacePath, filePath) - // Validate the raw user input (not the resolved path) to avoid false positives - this.validateFilePath(filePath, resolvedPath) - - try { - // Ensure the directory exists - const dir = path.dirname(resolvedPath) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - - await this.nodeFileStore.writeFile(filePath, contents) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to write file: ${path.basename(filePath)}`) - } - } - - /** - * Gets the workspace configuration - */ - getConfig(): Readonly> { - return { ...this.config } - } - - /** - * Creates a secure file store with workspace enforcement disabled (for backward compatibility) - * WARNING: This should only be used when absolutely necessary and with proper user consent - */ - static createUnsecure(basePath?: string): SecureFileStore { - const workspacePath = basePath || process.cwd() - return new SecureFileStore({ - workspacePath, - enforceWorkspaceBoundaries: false, - maxFileSize: 50 * 1024 * 1024, // 50MB for insecure mode - blockedExtensions: [] // No extension restrictions in insecure mode - }) - } -} diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index f185f1811..5a72144f0 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -69,149 +69,3 @@ 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 - * @param {string} workspacePath The workspace base path - * @returns {boolean} True if path is within workspace, false otherwise - */ -export const isWithinWorkspace = (filePath: string, workspacePath: string): boolean => { - if (!filePath || !workspacePath) { - return false - } - - try { - const path = require('path') - - // Resolve both paths to absolute paths - const resolvedFilePath = path.resolve(workspacePath, filePath) - const resolvedWorkspacePath = path.resolve(workspacePath) - - // Normalize paths to handle different separators - const normalizedFilePath = path.normalize(resolvedFilePath) - const normalizedWorkspacePath = path.normalize(resolvedWorkspacePath) - - // Check if the file path starts with the workspace path - const relativePath = path.relative(normalizedWorkspacePath, normalizedFilePath) - - // If relative path starts with '..' or is absolute, it's outside workspace - return !relativePath.startsWith('..') && !path.isAbsolute(relativePath) - } catch (error) { - // If any error occurs during path resolution, deny access - 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 - } -}