Skip to main content
Glama
security.ts8.86 kB
import * as fs from "node:fs/promises"; import * as path from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import YAML from "yaml"; const execp = promisify(execFile); export interface SecurityConfig { maxFileSize: number; allowedExtensions: string[]; allowedPaths: string[]; blockAbsolutePaths: boolean; blockTraversal: boolean; source: "reviewextention" | "local" | "default"; timestamp: string; } const DEFAULT_SECURITY: SecurityConfig = { maxFileSize: 1024 * 1024, allowedExtensions: [".txt", ".re", ".rb", ".cs", ".java", ".py", ".js", ".ts"], allowedPaths: ["code/", "src/", "lib/", "examples/"], blockAbsolutePaths: true, blockTraversal: true, source: "default", timestamp: new Date().toISOString() }; let cachedConfig: SecurityConfig | null = null; let cacheExpiry: number = 0; export async function loadSecurityConfig(cwd: string, forceReload = false): Promise<SecurityConfig> { const now = Date.now(); if (!forceReload && cachedConfig && now < cacheExpiry) { console.log("[Security] Using cached SSOT config"); return cachedConfig; } console.log("[Security] Loading SSOT configuration..."); try { const reviewExtConfig = await loadFromReviewExtention(cwd); if (reviewExtConfig) { cachedConfig = reviewExtConfig; cacheExpiry = now + 5 * 60 * 1000; logConfigSource(reviewExtConfig); return reviewExtConfig; } } catch (error) { console.warn("[Security] Failed to load from ReviewExtention:", error); } try { const localConfig = await loadFromLocalConfig(cwd); if (localConfig) { cachedConfig = localConfig; cacheExpiry = now + 5 * 60 * 1000; logConfigSource(localConfig); return localConfig; } } catch (error) { console.warn("[Security] Failed to load local config:", error); } console.log("[Security] Using default configuration"); cachedConfig = DEFAULT_SECURITY; cacheExpiry = now + 5 * 60 * 1000; logConfigSource(DEFAULT_SECURITY); return DEFAULT_SECURITY; } async function loadFromReviewExtention(cwd: string): Promise<SecurityConfig | null> { try { const rubyScript = ` begin require_relative './review-ext.rb' require 'json' config = { max_file_size: defined?(MAX_FILE_SIZE) ? MAX_FILE_SIZE : 1048576, allowed_extensions: defined?(ALLOWED_EXTENSIONS) ? ALLOWED_EXTENSIONS : [], allowed_paths: defined?(ALLOWED_PATHS) ? ALLOWED_PATHS : [], block_absolute_paths: defined?(BLOCK_ABSOLUTE_PATHS) ? BLOCK_ABSOLUTE_PATHS : true, block_traversal: defined?(BLOCK_TRAVERSAL) ? BLOCK_TRAVERSAL : true } puts JSON.generate(config) rescue LoadError => e exit 1 end `; const result = await execp("ruby", ["-e", rubyScript], { cwd, timeout: 5000 }); const parsed = JSON.parse(result.stdout); return { maxFileSize: parsed.max_file_size, allowedExtensions: parsed.allowed_extensions, allowedPaths: parsed.allowed_paths, blockAbsolutePaths: parsed.block_absolute_paths, blockTraversal: parsed.block_traversal, source: "reviewextention", timestamp: new Date().toISOString() }; } catch { return null; } } async function loadFromLocalConfig(cwd: string): Promise<SecurityConfig | null> { const configPaths = [ path.join(cwd, "config", "security.yml"), path.join(cwd, "config", "security.yaml"), path.join(cwd, "security.yml"), path.join(cwd, ".review-security.yml") ]; for (const configPath of configPaths) { try { const content = await fs.readFile(configPath, "utf-8"); const parsed = YAML.parse(content); if (parsed.security) { return { maxFileSize: parsed.security.max_file_size || DEFAULT_SECURITY.maxFileSize, allowedExtensions: parsed.security.allowed_extensions || DEFAULT_SECURITY.allowedExtensions, allowedPaths: parsed.security.allowed_paths || DEFAULT_SECURITY.allowedPaths, blockAbsolutePaths: parsed.security.block_absolute_paths ?? DEFAULT_SECURITY.blockAbsolutePaths, blockTraversal: parsed.security.block_traversal ?? DEFAULT_SECURITY.blockTraversal, source: "local", timestamp: new Date().toISOString() }; } } catch { continue; } } return null; } export function validateMapfilePath( filepath: string, config: SecurityConfig ): { valid: boolean; reason?: string } { if (config.blockAbsolutePaths && path.isAbsolute(filepath)) { return { valid: false, reason: "Absolute paths are not allowed" }; } if (config.blockTraversal && (filepath.includes("../") || filepath.includes("..\\"))) { return { valid: false, reason: "Path traversal is not allowed" }; } const ext = path.extname(filepath).toLowerCase(); if (config.allowedExtensions.length > 0 && !config.allowedExtensions.includes(ext)) { return { valid: false, reason: `File extension '${ext}' is not allowed. Allowed: ${config.allowedExtensions.join(", ")}` }; } const normalizedPath = filepath.replace(/\\/g, "/"); const isInAllowedPath = config.allowedPaths.length === 0 || config.allowedPaths.some(allowed => normalizedPath.startsWith(allowed)); if (!isInAllowedPath) { return { valid: false, reason: `Path must be within allowed directories: ${config.allowedPaths.join(", ")}` }; } return { valid: true }; } export async function validateMapfileSize( filepath: string, cwd: string, config: SecurityConfig ): Promise<{ valid: boolean; reason?: string; size?: number }> { const fullPath = path.join(cwd, filepath); try { const stats = await fs.stat(fullPath); if (stats.size > config.maxFileSize) { return { valid: false, reason: `File size (${stats.size} bytes) exceeds maximum allowed size (${config.maxFileSize} bytes)`, size: stats.size }; } return { valid: true, size: stats.size }; } catch (error: any) { return { valid: false, reason: `Cannot access file: ${error.message}` }; } } export async function sanitizeMapfile( content: string, filepath: string, config: SecurityConfig ): Promise<{ safe: boolean; sanitized?: string; issues: string[] }> { const issues: string[] = []; const suspiciousPatterns = [ /eval\s*\(/gi, /require\s*\(/gi, /import\s+/gi, /__import__/gi, /exec\s*\(/gi, /system\s*\(/gi, /`[^`]*`/g ]; for (const pattern of suspiciousPatterns) { if (pattern.test(content)) { issues.push(`Suspicious pattern detected: ${pattern.source}`); } } if (issues.length > 0) { return { safe: false, issues }; } return { safe: true, sanitized: content, issues: [] }; } function logConfigSource(config: SecurityConfig) { console.log(`[Security] Configuration loaded from: ${config.source}`); console.log(`[Security] Max file size: ${config.maxFileSize} bytes`); console.log(`[Security] Allowed extensions: ${config.allowedExtensions.join(", ")}`); console.log(`[Security] Allowed paths: ${config.allowedPaths.join(", ")}`); console.log(`[Security] Block absolute paths: ${config.blockAbsolutePaths}`); console.log(`[Security] Block traversal: ${config.blockTraversal}`); } export async function compareWithReviewExtention( cwd: string, currentConfig: SecurityConfig ): Promise<{ matching: boolean; differences: string[] }> { const reviewExtConfig = await loadFromReviewExtention(cwd); if (!reviewExtConfig) { return { matching: false, differences: ["ReviewExtention config not available"] }; } const differences: string[] = []; if (currentConfig.maxFileSize !== reviewExtConfig.maxFileSize) { differences.push(`Max file size: MCP=${currentConfig.maxFileSize}, ReviewExt=${reviewExtConfig.maxFileSize}`); } const extDiff = arrayDifference(currentConfig.allowedExtensions, reviewExtConfig.allowedExtensions); if (extDiff.length > 0) { differences.push(`Allowed extensions differ: ${extDiff.join(", ")}`); } const pathDiff = arrayDifference(currentConfig.allowedPaths, reviewExtConfig.allowedPaths); if (pathDiff.length > 0) { differences.push(`Allowed paths differ: ${pathDiff.join(", ")}`); } return { matching: differences.length === 0, differences }; } function arrayDifference(arr1: string[], arr2: string[]): string[] { const set1 = new Set(arr1); const set2 = new Set(arr2); const diff: string[] = []; for (const item of set1) { if (!set2.has(item)) diff.push(`+${item}`); } for (const item of set2) { if (!set1.has(item)) diff.push(`-${item}`); } return diff; }

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/dsgarage/ReviewMCP'

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