Skip to main content
Glama
helpers.ts6.16 kB
/** * Shared helper functions for all plugins */ /** * Validates that required parameters are present */ export function validateRequiredParams(params: any, required: string[]): void { for (const param of required) { if (!params[param]) { throw new Error(`Missing required parameter: ${param}`); } } } /** * Safely validates and normalizes a file path to prevent path traversal attacks * This function should be used before any file operations */ export async function validateAndNormalizePath(filePath: string): Promise<string> { const path = await import('path'); const { securityConfig } = await import('../../security-config.js'); // Input validation if (!filePath || typeof filePath !== 'string') { throw new Error('Invalid file path provided'); } // Check for null bytes (directory traversal attack vector) if (filePath.includes('\0')) { throw new Error('Access denied: Null byte detected in file path'); } // Check for path traversal sequences BEFORE normalization (prevents bypass) if (filePath.includes('..')) { throw new Error('Access denied: Path traversal sequences detected'); } // Must be absolute path if (!path.isAbsolute(filePath)) { throw new Error('File path must be absolute'); } // Normalize and resolve to canonical form - this MUST come after traversal checks const normalizedPath = path.resolve(path.normalize(filePath)); // Double-check: ensure normalization didn't introduce traversal sequences if (normalizedPath.includes('..')) { throw new Error('Access denied: Path normalization resulted in traversal sequences'); } // Get allowed directories (these are already normalized and lowercase for Windows) const allowedDirs = securityConfig.getAllowedDirectories(); // Validate against allowed directories (case-insensitive for Windows) const normalizedPathLower = normalizedPath.toLowerCase(); const isPathSafe = allowedDirs.some(allowedDir => { // Exact match check first if (normalizedPathLower === allowedDir) { return true; } // Subdirectory check - ensure path is actually within the allowed directory // Both paths must end with separator for accurate boundary detection const normalizedAllowedDir = allowedDir.endsWith(path.sep) ? allowedDir : allowedDir + path.sep; const pathToCheck = normalizedPathLower.endsWith(path.sep) ? normalizedPathLower : normalizedPathLower + path.sep; return pathToCheck.startsWith(normalizedAllowedDir); }); if (!isPathSafe) { // Log security violation if enabled if (securityConfig.security.logSecurityViolations) { console.warn(`SECURITY VIOLATION: Attempted access to unauthorized path: ${filePath} (normalized: ${normalizedPath})`); console.warn(`Allowed directories: ${allowedDirs.join(', ')}`); } throw new Error(`Access denied: Path '${filePath}' is outside allowed directories`); } // Final validation: ensure the normalized path hasn't escaped the boundaries // This catches edge cases where path.resolve might behave unexpectedly const relativePath = path.relative(allowedDirs.find(dir => normalizedPathLower.startsWith(dir))!, normalizedPath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { throw new Error('Access denied: Path validation failed - potential escape detected'); } return normalizedPath; } /** * Safely reads file content with proper security checks and path traversal protection * Uses the validateAndNormalizePath function to ensure security */ export async function readFileContent(filePath: string): Promise<string> { const fs = await import('fs/promises'); const path = await import('path'); // Import security config const { securityConfig } = await import('../../security-config.js'); // First, validate and normalize the path (this includes all security checks) const normalizedPath = await validateAndNormalizePath(filePath); try { // Check if file exists and is actually a file (not directory) const stat = await fs.stat(normalizedPath); if (!stat.isFile()) { throw new Error(`Path '${filePath}' is not a file`); } // Check file size limit if (stat.size > securityConfig.security.maxFileSize) { throw new Error(`File '${filePath}' exceeds maximum size limit (${securityConfig.security.maxFileSize} bytes)`); } // Check file extension if restrictions are in place const fileExt = path.extname(normalizedPath).toLowerCase(); if (securityConfig.security.allowedExtensions.length > 0 && !securityConfig.security.allowedExtensions.includes(fileExt)) { throw new Error(`File extension '${fileExt}' is not allowed`); } // Read file content const content = await fs.readFile(normalizedPath, 'utf-8'); return content; } catch (error: any) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`); } else if (error.code === 'EACCES') { throw new Error(`Permission denied: ${filePath}`); } else { // Re-throw our custom errors as-is if (error.message.includes('Access denied') || error.message.includes('exceeds maximum size') || error.message.includes('not allowed')) { throw error; } throw new Error(`Failed to read file '${filePath}': ${error.message}`); } } } /** * Formats code snippets for inclusion in prompts */ export function formatCodeForPrompt(code: string, language: string): string { return `\`\`\`${language}\n${code}\n\`\`\``; } /** * Truncates long strings with ellipsis */ export function truncateString(str: string, maxLength: number): string { if (str.length <= maxLength) return str; return str.substring(0, maxLength - 3) + '...'; } /** * Escapes special characters in strings for safe prompt inclusion */ export function escapeForPrompt(str: string): string { return str.replace(/[`\\]/g, '\\$&'); } export default { validateRequiredParams, validateAndNormalizePath, readFileContent, formatCodeForPrompt, truncateString, escapeForPrompt };

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/houtini-ai/lm'

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