/** * Claude Code Plugin Security System * * Features: * - Permission validation * - File system sandboxing * - Command execution validation * - Network access control * - Resource limits * - Code injection prevention */ import fs from 'fs/promises' import path from 'path' import { createHash } from 'crypto' // ============================================================================ // TYPES AND INTERFACES // ============================================================================ export type Permission = | 'read:files' | 'write:files' | 'execute:commands' | 'network:request' | 'read:config' | 'write:config' | 'hook:events' | 'read:secrets' export interface SecurityPolicy { allowedPaths: string[] deniedPaths: string[] allowedCommands: string[] deniedCommands: string[] allowedDomains: string[] deniedDomains: string[] maxFileSize: number maxExecutionTime: number requireCodeSigning: boolean } export interface SecurityContext { pluginName: string permissions: Permission[] workingDirectory: string startTime: number } export interface ValidationResult { allowed: boolean reason?: string modifiedValue?: any } // ============================================================================ // SECURITY MANAGER CLASS // ============================================================================ export class SecurityManager { private policy: SecurityPolicy private contexts: Map = new Map() private auditLog: AuditLog[] = [] constructor(policy?: Partial) { this.policy = { allowedPaths: [], deniedPaths: [], allowedCommands: [], deniedCommands: ['rm -rf /', 'format', 'mkfs'], allowedDomains: [], deniedDomains: [], maxFileSize: 100 * 1024 * 1024, // 100MB maxExecutionTime: 30000, // 30 seconds requireCodeSigning: false, ...policy, } } // ============================================================================ // CONTEXT MANAGEMENT // ============================================================================ createContext(pluginName: string, permissions: Permission[]): SecurityContext { const context: SecurityContext = { pluginName, permissions, workingDirectory: process.cwd(), startTime: Date.now(), } this.contexts.set(pluginName, context) return context } getContext(pluginName: string): SecurityContext | undefined { return this.contexts.get(pluginName) } removeContext(pluginName: string): void { this.contexts.delete(pluginName) } // ============================================================================ // PERMISSION VALIDATION // ============================================================================ hasPermission(pluginName: string, permission: Permission): boolean { const context = this.getContext(pluginName) if (!context) return false return context.permissions.includes(permission) } validatePermissions( pluginName: string, requiredPermissions: Permission[] ): ValidationResult { const context = this.getContext(pluginName) if (!context) { return { allowed: false, reason: 'Plugin context not found', } } const missing = requiredPermissions.filter( (perm) => !context.permissions.includes(perm) ) if (missing.length > 0) { return { allowed: false, reason: `Missing permissions: ${missing.join(', ')}`, } } return { allowed: true } } // ============================================================================ // FILE SYSTEM VALIDATION // ============================================================================ async validateFileAccess( pluginName: string, filePath: string, mode: 'read' | 'write' ): Promise { const permission = mode === 'read' ? 'read:files' : 'write:files' if (!this.hasPermission(pluginName, permission)) { return { allowed: false, reason: `Missing permission: ${permission}`, } } const resolvedPath = path.resolve(filePath) // Check denied paths first for (const denied of this.policy.deniedPaths) { if (resolvedPath.startsWith(path.resolve(denied))) { return { allowed: false, reason: `Access denied to path: ${filePath}`, } } } // If allowed paths are specified, check against them if (this.policy.allowedPaths.length > 0) { const allowed = this.policy.allowedPaths.some((allowedPath) => resolvedPath.startsWith(path.resolve(allowedPath)) ) if (!allowed) { return { allowed: false, reason: `Path not in allowed list: ${filePath}`, } } } // Check file size for writes if (mode === 'write') { try { const stats = await fs.stat(resolvedPath) if (stats.size > this.policy.maxFileSize) { return { allowed: false, reason: `File too large: ${stats.size} bytes`, } } } catch { // File doesn't exist yet, that's fine for writes } } return { allowed: true } } // ============================================================================ // COMMAND VALIDATION // ============================================================================ validateCommand(pluginName: string, command: string): ValidationResult { if (!this.hasPermission(pluginName, 'execute:commands')) { return { allowed: false, reason: 'Missing permission: execute:commands', } } // Check denied commands for (const denied of this.policy.deniedCommands) { if (command.includes(denied)) { return { allowed: false, reason: `Command contains denied pattern: ${denied}`, } } } // Check for dangerous patterns const dangerousPatterns = [ /\brm\s+-rf\s+\//, /\bformat\s+[a-z]:/i, /\bdel\s+\/[sq]/i, /\>\s*\/dev\/[a-z]+/, /\|.*\brm\b/, /&&.*\brm\b/, /;.*\brm\b/, ] for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return { allowed: false, reason: 'Command contains dangerous pattern', } } } return { allowed: true } } // ============================================================================ // NETWORK VALIDATION // ============================================================================ validateNetworkRequest(pluginName: string, url: string): ValidationResult { if (!this.hasPermission(pluginName, 'network:request')) { return { allowed: false, reason: 'Missing permission: network:request', } } try { const urlObj = new URL(url) const domain = urlObj.hostname // Check denied domains for (const denied of this.policy.deniedDomains) { if (domain === denied || domain.endsWith(`.${denied}`)) { return { allowed: false, reason: `Access denied to domain: ${domain}`, } } } // If allowed domains are specified, check against them if (this.policy.allowedDomains.length > 0) { const allowed = this.policy.allowedDomains.some( (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) ) if (!allowed) { return { allowed: false, reason: `Domain not in allowed list: ${domain}`, } } } } catch { return { allowed: false, reason: 'Invalid URL', } } return { allowed: true } } // ============================================================================ // CODE INJECTION PREVENTION // ============================================================================ sanitizeInput(input: string): string { // Remove potential code injection patterns return input .replace(/]*>.*?<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') .replace(/\${.*?}/g, '') // Template literals .replace(/`.*?`/g, '') // Backtick expressions .replace(/eval\s*\(/gi, '') .replace(/Function\s*\(/gi, '') .replace(/setTimeout\s*\(/gi, '') .replace(/setInterval\s*\(/gi, '') } validateScriptCode(code: string): ValidationResult { const dangerousPatterns = [ /eval\s*\(/, /Function\s*\(/, /require\s*\(\s*['"`]fs['"`]\s*\)/, /require\s*\(\s*['"`]child_process['"`]\s*\)/, /process\s*\.\s*exit/, /\.\.\//, // Path traversal /~\//, // Home directory access ] for (const pattern of dangerousPatterns) { if (pattern.test(code)) { return { allowed: false, reason: `Code contains dangerous pattern: ${pattern.source}`, } } } return { allowed: true } } // ============================================================================ // INTEGRITY CHECKING // ============================================================================ async calculateFileIntegrity(filePath: string): Promise { const content = await fs.readFile(filePath) return createHash('sha256').update(content).digest('hex') } async verifyPluginIntegrity( pluginPath: string, expectedIntegrity: string ): Promise { const actualIntegrity = await this.calculateDirectoryIntegrity(pluginPath) return actualIntegrity === expectedIntegrity } async calculateDirectoryIntegrity(dirPath: string): Promise { const hash = createHash('sha256') const files = await this.getAllFiles(dirPath) for (const file of files.sort()) { const content = await fs.readFile(file) hash.update(content) } return hash.digest('hex') } private async getAllFiles(dirPath: string): Promise { const files: string[] = [] const entries = await fs.readdir(dirPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { files.push(...(await this.getAllFiles(fullPath))) } else { files.push(fullPath) } } return files } // ============================================================================ // AUDIT LOGGING // ============================================================================ logAccess( pluginName: string, action: string, resource: string, allowed: boolean ): void { const entry: AuditLog = { timestamp: new Date().toISOString(), pluginName, action, resource, allowed, } this.auditLog.push(entry) // Keep only last 1000 entries if (this.auditLog.length > 1000) { this.auditLog = this.auditLog.slice(-1000) } } getAuditLog(pluginName?: string): AuditLog[] { if (pluginName) { return this.auditLog.filter((entry) => entry.pluginName === pluginName) } return [...this.auditLog] } clearAuditLog(): void { this.auditLog = [] } } // ============================================================================ // TYPES // ============================================================================ interface AuditLog { timestamp: string pluginName: string action: string resource: string allowed: boolean } // ============================================================================ // SANDBOX CLASS - Isolated Execution Environment // ============================================================================ export class Sandbox { private security: SecurityManager private context: SecurityContext constructor(security: SecurityManager, context: SecurityContext) { this.security = security this.context = context } async readFile(filePath: string): Promise { const validation = await this.security.validateFileAccess( this.context.pluginName, filePath, 'read' ) if (!validation.allowed) { this.security.logAccess( this.context.pluginName, 'read:file', filePath, false ) throw new Error(validation.reason) } this.security.logAccess( this.context.pluginName, 'read:file', filePath, true ) return await fs.readFile(filePath, 'utf-8') } async writeFile(filePath: string, content: string): Promise { const validation = await this.security.validateFileAccess( this.context.pluginName, filePath, 'write' ) if (!validation.allowed) { this.security.logAccess( this.context.pluginName, 'write:file', filePath, false ) throw new Error(validation.reason) } this.security.logAccess( this.context.pluginName, 'write:file', filePath, true ) await fs.writeFile(filePath, content, 'utf-8') } executeCommand(command: string): Promise { const validation = this.security.validateCommand( this.context.pluginName, command ) if (!validation.allowed) { this.security.logAccess( this.context.pluginName, 'execute:command', command, false ) throw new Error(validation.reason) } this.security.logAccess( this.context.pluginName, 'execute:command', command, true ) // Command execution would be implemented here return Promise.resolve('') } } // ============================================================================ // EXPORTS // ============================================================================ export default SecurityManager