Skip to main content
Glama
security-validator.js•6.03 kB
import path from 'path'; import { access, stat } from 'fs/promises'; import { constants } from 'fs'; import { LIMITS } from './constants.js'; const { resolve, relative, join, dirname } = path; // Store process reference at module level to avoid conflicts const nodeProcess = process; /** * Security validator for filesystem operations * Ensures all paths are safe and within allowed boundaries */ export class SecurityValidator { constructor(options = {}) { // Default to current working directory as root this.rootPath = resolve(options.rootPath || nodeProcess.cwd()); this.maxFileSize = options.maxFileSize || LIMITS.MAX_FILE_SIZE; this.allowedExtensions = options.allowedExtensions || null; // null = all allowed this.blockedPatterns = [ /^\./, // Hidden files/directories /node_modules/, // Dependencies /\.git/, // Git directory /\.env/, // Environment files /\.(key|pem|cert|crt)$/, // Certificates and keys ]; } /** * Validates and resolves a path, ensuring it's within allowed boundaries * @param {string} inputPath - Path to validate * @returns {{valid: boolean, resolvedPath?: string, error?: string}} */ validatePath(inputPath) { try { // Resolve the path relative to root const resolvedPath = resolve(this.rootPath, inputPath); // Check if path is within root directory const relativePath = relative(this.rootPath, resolvedPath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return { valid: false, error: 'Path traversal detected - access denied', }; } // Check against blocked patterns for (const pattern of this.blockedPatterns) { if (pattern.test(relativePath)) { return { valid: false, error: `Access to ${pattern.toString()} is blocked`, }; } } // Check file extension if restrictions are set if (this.allowedExtensions && this.allowedExtensions.length > 0) { const ext = path.extname(resolvedPath).toLowerCase(); if (!this.allowedExtensions.includes(ext)) { return { valid: false, error: `File extension ${ext} is not allowed`, }; } } return { valid: true, resolvedPath, }; } catch (error) { return { valid: false, error: `Path validation error: ${error.message}`, }; } } /** * Validates file size * @param {string} filePath - Path to file * @returns {Promise<{valid: boolean, size?: number, error?: string}>} */ async validateFileSize(filePath) { try { const stats = await stat(filePath); const size = stats.size; if (size > this.maxFileSize) { return { valid: false, size, error: `File size ${size} bytes exceeds maximum allowed size of ${this.maxFileSize} bytes`, }; } return { valid: true, size, }; } catch (error) { return { valid: false, error: `Failed to check file size: ${error.message}`, }; } } /** * Checks if path exists and is accessible * @param {string} filePath - Path to check * @param {number} mode - Access mode (default: read) * @returns {Promise<boolean>} */ async pathExists(filePath, mode = constants.R_OK) { try { await access(filePath, mode); return true; } catch { return false; } } /** * Sanitizes a filename to prevent directory traversal * @param {string} filename - Filename to sanitize * @returns {string} Sanitized filename */ sanitizeFilename(filename) { if (!filename) return 'file'; // Remove any path separators and parent directory references return filename .replace(/[\/\\]/g, '_') .replace(/\.\./g, '_') .replace(/^\./, '_') .substring(0, 255); // Limit length } /** * Validates operation permissions * @param {string} operation - Operation type (create, read, update, delete) * @param {string} filePath - Target file path * @returns {Promise<{allowed: boolean, requiresConfirmation?: boolean, reason?: string}>} */ async validateOperation(operation, filePath) { const pathValidation = this.validatePath(filePath); if (!pathValidation.valid) { return { allowed: false, reason: pathValidation.error, }; } // Destructive operations require confirmation const destructiveOps = ['delete', 'update', 'move']; const requiresConfirmation = destructiveOps.includes(operation); // Check if file exists for operations that require it const requiresExistence = ['read', 'update', 'delete', 'move']; if (requiresExistence.includes(operation)) { const exists = await this.pathExists(pathValidation.resolvedPath); if (!exists) { return { allowed: false, reason: 'File or directory does not exist', }; } } // Check if path already exists for create operations if (operation === 'create') { const exists = await this.pathExists(pathValidation.resolvedPath); if (exists) { return { allowed: false, reason: 'File or directory already exists', }; } } return { allowed: true, requiresConfirmation, resolvedPath: pathValidation.resolvedPath, }; } /** * Creates a safe backup path for a file * @param {string} filePath - Original file path * @returns {string} Backup file path */ createBackupPath(filePath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const dir = dirname(filePath); const filename = path.basename(filePath); return join(dir, `.backup-${timestamp}-${filename}`); } } // Export a default instance export const defaultValidator = new SecurityValidator();

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/moikas-code/moidvk'

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