remove read write file tools and imports
This commit is contained in:
parent
366d38b861
commit
5c0e66f2f1
|
|
@ -10,7 +10,6 @@ import { test } from 'linkifyjs'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src'
|
import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src'
|
||||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { isSafeBrowserExecutable } from '../../../src/validator'
|
|
||||||
|
|
||||||
class Playwright_DocumentLoaders implements INode {
|
class Playwright_DocumentLoaders implements INode {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -193,9 +192,6 @@ class Playwright_DocumentLoaders implements INode {
|
||||||
let docs = []
|
let docs = []
|
||||||
|
|
||||||
const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH
|
const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH
|
||||||
if (!isSafeBrowserExecutable(executablePath)) {
|
|
||||||
throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `)
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: PlaywrightWebBaseLoaderOptions = {
|
const config: PlaywrightWebBaseLoaderOptions = {
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { omit } from 'lodash'
|
||||||
import { PuppeteerLifeCycleEvent } from 'puppeteer'
|
import { PuppeteerLifeCycleEvent } from 'puppeteer'
|
||||||
import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src'
|
import { handleEscapeCharacters, INodeOutputsValue, webCrawl, xmlScrape } from '../../../src'
|
||||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { isSafeBrowserExecutable } from '../../../src/validator'
|
|
||||||
|
|
||||||
class Puppeteer_DocumentLoaders implements INode {
|
class Puppeteer_DocumentLoaders implements INode {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -184,9 +183,6 @@ class Puppeteer_DocumentLoaders implements INode {
|
||||||
let docs: Document[] = []
|
let docs: Document[] = []
|
||||||
|
|
||||||
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|
||||||
if (!isSafeBrowserExecutable(executablePath)) {
|
|
||||||
throw new Error(`Invalid or unsafe browser executable path: ${executablePath || 'undefined'}. `)
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: PuppeteerWebBaseLoaderOptions = {
|
const config: PuppeteerWebBaseLoaderOptions = {
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
|
|
|
||||||
|
|
@ -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<string>
|
|
||||||
abstract writeFile(path: string, contents: string): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<any> {
|
|
||||||
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<typeof this.schema>) {
|
|
||||||
return await this.store.readFile(file_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { nodeClass: ReadFile_Tools }
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18 5H9C7.89543 5 7 5.89543 7 7V25C7 26.1046 7.89543 27 9 27H12M18 5L25 12M18 5V12H25M25 12V25C25 26.1046 24.1046 27 23 27H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M16 17V29M16 17L13 20.1361M16 17L19 20.1361" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 455 B |
|
|
@ -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<string>
|
|
||||||
abstract writeFile(path: string, contents: string): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<any> {
|
|
||||||
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<typeof this.schema>) {
|
|
||||||
await this.store.writeFile(file_path, text)
|
|
||||||
return `File written to ${file_path} successfully.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { nodeClass: WriteFile_Tools }
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M25 18V25C25 26.1046 24.1046 27 23 27H9C7.89543 27 7 26.1046 7 25V7C7 5.89543 7.89543 5 9 5H18L19 6" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 19.3284V22H14.6716C15.202 22 15.7107 21.7893 16.0858 21.4142L24.5858 12.9142C25.3668 12.1332 25.3668 10.8668 24.5858 10.0858L23.9142 9.41421C23.1332 8.63316 21.8668 8.63317 21.0858 9.41421L12.5858 17.9142C12.2107 18.2893 12 18.798 12 19.3284Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 632 B |
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -69,149 +69,3 @@ export const isUnsafeFilePath = (filePath: string): boolean => {
|
||||||
|
|
||||||
return dangerousPatterns.some((pattern) => pattern.test(filePath))
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue