Skip to main content
Glama
security.ts8.45 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import type { ConfigLoader } from './config.js'; import { getContainer, ServiceKeys } from './di/Container.js'; import type { Logger } from './logger.js'; /** * Security utilities for Bruno MCP Server */ /** * Validate if a path is within allowed directories (with DI) */ export async function validatePathWithConfig(targetPath: string, configLoader: ConfigLoader): Promise<{ valid: boolean; error?: string }> { const security = configLoader.getSecurity(); // If no allowed paths configured, allow all paths if (!security.allowedPaths || security.allowedPaths.length === 0) { return { valid: true }; } try { // Resolve to absolute path const absolutePath = path.resolve(targetPath); // Check if path exists try { await fs.access(absolutePath); } catch { return { valid: false, error: `Path does not exist: ${targetPath}` }; } // Check if path is within any allowed directory for (const allowedPath of security.allowedPaths) { const absoluteAllowedPath = path.resolve(allowedPath); const relativePath = path.relative(absoluteAllowedPath, absolutePath); // If relative path doesn't start with '..' it means it's inside the allowed path if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) { return { valid: true }; } } return { valid: false, error: `Path is not within allowed directories: ${targetPath}. Allowed paths: ${security.allowedPaths.join(', ')}` }; } catch (error) { return { valid: false, error: `Path validation error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Sanitize input to prevent command injection */ export function sanitizeInput(input: string): string { // Remove or escape potentially dangerous characters // Allow alphanumeric, common path characters, and safe punctuation return input.replace(/[;&|`$(){}[\]<>\\]/g, ''); } /** * Validate environment variable name */ export function validateEnvVarName(name: string): boolean { // Environment variable names should only contain alphanumeric characters and underscores return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name); } /** * Validate environment variable value */ export function validateEnvVarValue(value: string): boolean { // Check for potentially malicious patterns const dangerousPatterns = [ /[;&|`$()]/, // Command injection characters /\$\{.*\}/, // Variable expansion /\$\(.*\)/, // Command substitution /`.*`/, // Backtick command execution ]; return !dangerousPatterns.some(pattern => pattern.test(value)); } /** * Sanitize environment variables */ export function sanitizeEnvVariables(envVars: Record<string, string>): { sanitized: Record<string, string>; warnings: string[]; } { const sanitized: Record<string, string> = {}; const warnings: string[] = []; for (const [key, value] of Object.entries(envVars)) { // Validate key if (!validateEnvVarName(key)) { warnings.push(`Invalid environment variable name: ${key}`); continue; } // Validate value if (!validateEnvVarValue(value)) { warnings.push(`Potentially unsafe environment variable value for: ${key}`); continue; } sanitized[key] = value; } return { sanitized, warnings }; } /** * Validate if a path is within allowed directories (uses DI container) */ export async function validatePath(targetPath: string): Promise<{ valid: boolean; error?: string }> { const container = getContainer(); const configLoader = container.get<ConfigLoader>(ServiceKeys.CONFIG_LOADER); return validatePathWithConfig(targetPath, configLoader); } /** * Mask secrets in error messages (with DI) */ export function maskSecretsInErrorWithConfig(error: Error, configLoader: ConfigLoader): Error { const maskedMessage = configLoader.maskSecrets(error.message); const maskedStack = error.stack ? configLoader.maskSecrets(error.stack) : undefined; const maskedError = new Error(maskedMessage); maskedError.name = error.name; maskedError.stack = maskedStack; return maskedError; } /** * Mask secrets in error messages (uses DI container) */ export function maskSecretsInError(error: Error): Error { const container = getContainer(); const configLoader = container.get<ConfigLoader>(ServiceKeys.CONFIG_LOADER); return maskSecretsInErrorWithConfig(error, configLoader); } /** * Validate request name for safety */ export function validateRequestName(name: string): { valid: boolean; error?: string } { // Request names should not contain path traversal attempts if (name.includes('..') || name.includes('\\') || name.startsWith('/')) { return { valid: false, error: 'Request name contains invalid characters (path traversal attempt)' }; } // Check for null bytes if (name.includes('\0')) { return { valid: false, error: 'Request name contains null bytes' }; } return { valid: true }; } /** * Validate folder path for safety */ export function validateFolderPath(folderPath: string): { valid: boolean; error?: string } { // Folder paths should be relative and not contain path traversal if (folderPath.includes('..')) { return { valid: false, error: 'Folder path contains path traversal attempts (..)' }; } // Should not be absolute path if (path.isAbsolute(folderPath)) { return { valid: false, error: 'Folder path must be relative to collection root' }; } return { valid: true }; } /** * Comprehensive security validation for tool parameters (with DI) */ export async function validateToolParametersWithConfig( params: { collectionPath?: string; requestName?: string; folderPath?: string; envVariables?: Record<string, string>; }, configLoader: ConfigLoader ): Promise<{ valid: boolean; errors: string[]; warnings: string[] }> { const errors: string[] = []; const warnings: string[] = []; // Validate collection path if (params.collectionPath) { const pathValidation = await validatePathWithConfig(params.collectionPath, configLoader); if (!pathValidation.valid) { errors.push(pathValidation.error || 'Invalid collection path'); } } // Validate request name if (params.requestName) { const nameValidation = validateRequestName(params.requestName); if (!nameValidation.valid) { errors.push(nameValidation.error || 'Invalid request name'); } } // Validate folder path if (params.folderPath) { const folderValidation = validateFolderPath(params.folderPath); if (!folderValidation.valid) { errors.push(folderValidation.error || 'Invalid folder path'); } } // Validate environment variables if (params.envVariables) { const { warnings: envWarnings } = sanitizeEnvVariables(params.envVariables); warnings.push(...envWarnings); } return { valid: errors.length === 0, errors, warnings }; } /** * Comprehensive security validation for tool parameters (uses DI container) */ export async function validateToolParameters( params: { collectionPath?: string; requestName?: string; folderPath?: string; envVariables?: Record<string, string>; } ): Promise<{ valid: boolean; errors: string[]; warnings: string[] }> { const container = getContainer(); const configLoader = container.get<ConfigLoader>(ServiceKeys.CONFIG_LOADER); return validateToolParametersWithConfig(params, configLoader); } /** * Security audit logger (with DI) */ export function logSecurityEventWithLogger( event: { type: 'path_validation' | 'input_sanitization' | 'env_var_validation' | 'access_denied'; details: string; severity: 'info' | 'warning' | 'error'; }, logger: Logger ): void { // Use logger's logSecurityEvent method logger.logSecurityEvent(event.type, event.details, event.severity); } /** * Security audit logger (uses DI container) */ export function logSecurityEvent( event: { type: 'path_validation' | 'input_sanitization' | 'env_var_validation' | 'access_denied'; details: string; severity: 'info' | 'warning' | 'error'; } ): void { const container = getContainer(); const logger = container.get<Logger>(ServiceKeys.LOGGER); logSecurityEventWithLogger(event, logger); }

Latest Blog Posts

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/jcr82/bruno-mcp-server'

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