remove read write file tools and imports (#5480)

This commit is contained in:
Henry Heng
2025-11-15 20:24:42 +00:00
committed by GitHub
parent 366d38b861
commit 3cab803918
8 changed files with 0 additions and 654 deletions
-196
View File
@@ -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<FileSecurityConfig>
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<string> {
// 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<void> {
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<Required<FileSecurityConfig>> {
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
})
}
}
-146
View File
@@ -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
}
}