Skip to main content
Glama

Watchtower DAP Windows Debugging

by rlaksana
validation.ts13 kB
/** * Security Validation System * * Provides input validation, path traversal protection, and * injection attack prevention for the Watchtower debugging system. */ import { basename, join, normalize, resolve } from 'path'; import { homedir } from 'os'; /** * Path validation result */ export interface ValidationResult { isValid: boolean; errors: string[]; warnings: string[]; normalizedPath?: string; } /** * Validation rules */ export const ValidationRules = { // Path validation MAX_PATH_LENGTH: 4096, ALLOWED_EXTENSIONS: ['.js', '.ts', '.py', '.cs', '.dart', '.json', '.txt', '.md'], DISALLOWED_PATTERNS: [ /\.\./, // Parent directory traversal /\/\.\./, // Parent directory traversal /\\\\\.\./, // Windows parent directory traversal /<|>|:|"|'|`|;|&|\|/, // Command injection characters /\x00/, // Null byte ], // File name validation MAX_FILENAME_LENGTH: 255, DISALLOWED_FILENAMES: [ 'con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9', ], // Command validation ALLOWED_COMMANDS: ['node', 'python', 'dotnet', 'dart', 'vsdbg', 'netcoredbg', 'debugpy'], DISALLOWED_ARGS: [ '--eval', '-e', // Command execution '--exec', // Command execution '&&', '||', ';', // Command chaining '$(', '${', // Command substitution '|', '>', '<', '>>', // Redirection ], }; /** * Security Validator class */ export class SecurityValidator { private allowedBasePaths: Set<string>; private allowedCommands: Set<string>; constructor() { this.allowedBasePaths = new Set([ process.cwd(), homedir(), join(homedir(), 'projects'), join(homedir(), 'documents'), ]); this.allowedCommands = new Set(ValidationRules.ALLOWED_COMMANDS); } /** * Validate and normalize a file path */ validatePath(path: string, basePath?: string): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], }; if (!path || typeof path !== 'string') { result.isValid = false; result.errors.push('Path cannot be empty'); return result; } // Check path length if (path.length > ValidationRules.MAX_PATH_LENGTH) { result.isValid = false; result.errors.push( `Path exceeds maximum length of ${ValidationRules.MAX_PATH_LENGTH} characters` ); } // Check for path traversal if (this.hasPathTraversal(path)) { result.isValid = false; result.errors.push('Path traversal detected (../)'); } // Check for injection characters if (this.hasInjectionCharacters(path)) { result.isValid = false; result.errors.push('Invalid characters in path'); } // Check for reserved Windows filenames const filename = basename(path); if (ValidationRules.DISALLOWED_FILENAMES.includes(filename.toLowerCase())) { result.isValid = false; result.errors.push(`Filename "${filename}" is reserved on Windows`); } // Check filename length if (filename.length > ValidationRules.MAX_FILENAME_LENGTH) { result.isValid = false; result.errors.push( `Filename exceeds maximum length of ${ValidationRules.MAX_FILENAME_LENGTH} characters` ); } // Normalize path try { const normalized = normalize(path); result.normalizedPath = normalized; // Check if path is within allowed base directories if (basePath) { const resolvedPath = resolve(normalized); const resolvedBase = resolve(basePath); if (!resolvedPath.startsWith(resolvedBase)) { result.isValid = false; result.errors.push('Path is outside allowed base directory'); } } // Check if path exists if it's a file if (this.isFile(path)) { const extension = '.' + path.split('.').pop()?.toLowerCase(); if (!ValidationRules.ALLOWED_EXTENSIONS.includes(extension || '')) { result.warnings.push(`File extension "${extension}" is not explicitly allowed`); } } } catch (error) { result.isValid = false; result.errors.push(`Invalid path: ${(error as Error).message}`); } return result; } /** * Validate a command and its arguments */ validateCommand(command: string, args: string[] = []): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], }; // Check command if (!command || typeof command !== 'string') { result.isValid = false; result.errors.push('Command cannot be empty'); return result; } const commandName = this.extractCommandName(command); // Check if command is allowed if (!this.allowedCommands.has(commandName)) { result.isValid = false; result.errors.push(`Command "${commandName}" is not allowed`); } // Check arguments for (const arg of args) { if (!this.validateArgument(arg).isValid) { result.isValid = false; result.errors.push(`Invalid argument: ${arg}`); } } return result; } /** * Validate a single argument */ validateArgument(arg: string): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], }; if (!arg || typeof arg !== 'string') { result.isValid = false; result.errors.push('Argument cannot be empty'); return result; } // Check for argument injection if (this.hasInjectionCharacters(arg)) { result.isValid = false; result.errors.push('Invalid characters in argument'); } // Check for disallowed patterns for (const pattern of ValidationRules.DISALLOWED_ARGS) { if (arg.includes(pattern)) { result.isValid = false; result.errors.push(`Disallowed argument pattern: ${pattern}`); } } return result; } /** * Validate environment variables */ validateEnvironment(env: Record<string, string>): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], }; const sensitiveKeys = ['password', 'secret', 'token', 'key', 'auth']; const redacted = new Set<string>(); for (const [key, value] of Object.entries(env)) { // Check for sensitive keys that should not be logged const lowerKey = key.toLowerCase(); if (sensitiveKeys.some(sensitiveKey => lowerKey.includes(sensitiveKey))) { redacted.add(key); } // Validate key format if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { result.warnings.push(`Environment variable key "${key}" has non-standard characters`); } // Validate value if (this.hasInjectionCharacters(value)) { result.isValid = false; result.errors.push(`Invalid characters in environment variable "${key}"`); } } if (redacted.size > 0) { result.warnings.push(`Found ${redacted.size} potentially sensitive environment variables`); } return result; } /** * Validate debug configuration */ validateDebugConfig(config: any): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], }; // Check program path if (config.program) { const programValidation = this.validatePath(config.program); if (!programValidation.isValid) { result.isValid = false; result.errors.push(...programValidation.errors); } result.warnings.push(...programValidation.warnings); } // Check working directory if (config.cwd) { const cwdValidation = this.validatePath(config.cwd); if (!cwdValidation.isValid) { result.isValid = false; result.errors.push(...cwdValidation.errors); } result.warnings.push(...cwdValidation.warnings); } // Check arguments if (config.args && Array.isArray(config.args)) { for (const arg of config.args) { const argValidation = this.validateArgument(String(arg)); if (!argValidation.isValid) { result.isValid = false; result.errors.push(...argValidation.errors); } } } // Check environment variables if (config.env && typeof config.env === 'object') { const envValidation = this.validateEnvironment(config.env); if (!envValidation.isValid) { result.isValid = false; result.errors.push(...envValidation.errors); } result.warnings.push(...envValidation.warnings); } return result; } /** * Check for path traversal */ private hasPathTraversal(path: string): boolean { // Normalize and check for parent directory references const normalized = normalize(path); return normalized !== path && /(?:^|[\\\/])\.\.([\\\/]|$)/.test(normalized); } /** * Check for injection characters */ private hasInjectionCharacters(input: string): boolean { return ValidationRules.DISALLOWED_PATTERNS.some(pattern => pattern.test(input)); } /** * Extract command name from full path */ private extractCommandName(command: string): string { const base = basename(command); // Remove file extension for commands like node.exe, python.exe return base.split('.')[0]?.toLowerCase() || ''; } /** * Check if path is a file */ private isFile(path: string): boolean { // This is a simplified check - in a real implementation, // you would use fs.statSync to check if it's a file try { const fs = require('fs'); return fs.statSync(path).isFile(); } catch { return false; } } /** * Add allowed base path */ addAllowedBasePath(path: string): void { this.allowedBasePaths.add(resolve(path)); } /** * Add allowed command */ addAllowedCommand(command: string): void { this.allowedCommands.add(command.toLowerCase()); } /** * Remove allowed base path */ removeAllowedBasePath(path: string): void { this.allowedBasePaths.delete(resolve(path)); } /** * Remove allowed command */ removeAllowedCommand(command: string): void { this.allowedCommands.delete(command.toLowerCase()); } /** * Get validation summary */ getSummary(): { allowedBasePaths: string[]; allowedCommands: string[]; } { return { allowedBasePaths: Array.from(this.allowedBasePaths), allowedCommands: Array.from(this.allowedCommands), }; } } /** * Global security validator instance */ let globalValidator: SecurityValidator | null = null; /** * Get or create global security validator */ export function getGlobalValidator(): SecurityValidator { if (!globalValidator) { globalValidator = new SecurityValidator(); } return globalValidator; } /** * Convenience functions for validation */ export function validatePath(path: string, basePath?: string): ValidationResult { return getGlobalValidator().validatePath(path, basePath); } export function validateCommand(command: string, args?: string[]): ValidationResult { return getGlobalValidator().validateCommand(command, args); } export function validateArgument(arg: string): ValidationResult { return getGlobalValidator().validateArgument(arg); } export function validateEnvironment(env: Record<string, string>): ValidationResult { return getGlobalValidator().validateEnvironment(env); } export function validateDebugConfig(config: any): ValidationResult { return getGlobalValidator().validateDebugConfig(config); } /** * Create security validator with custom configuration */ export function createSecurityValidator(): SecurityValidator { return new SecurityValidator(); } /** * Sanitize user input */ export function sanitizeInput(input: any): string { if (typeof input !== 'string') { return String(input); } // Remove or replace potentially dangerous characters return input .replace(/[<>"'`;&|]/g, '') // Remove shell injection characters .replace(/\x00/g, '') // Remove null bytes .trim(); } /** * Check if input is safe for logging */ export function isSafeForLogging(input: any): boolean { if (typeof input !== 'string') { return true; } // Check for sensitive patterns const sensitivePatterns = [ /password\s*[:=]\s*\S+/i, /token\s*[:=]\s*\S+/i, /secret\s*[:=]\s*\S+/i, /api[_-]?key\s*[:=]\s*\S+/i, /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*/, // JWT ]; return !sensitivePatterns.some(pattern => pattern.test(input)); } /** * Escape shell arguments safely */ export function escapeShellArg(arg: string): string { // Simple escaping for Windows return arg.replace(/["\\]/g, '\\$&'); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rlaksana/mcp-watchtower'

If you have feedback or need assistance with the MCP directory API, please join our Discord server