Skip to main content
Glama
reuvenaor

Shadcn Registry manager

by reuvenaor
secure-exec.ts9.53 kB
import { execa, type ExecaReturnValue, type Options as ExecaOptions } from "execa" import { validateWorkingDirectory } from "./security" // Allowlist of commands that are permitted to be executed const ALLOWED_COMMANDS = [ 'npm', 'pnpm', 'yarn', 'npx', 'git', 'tar', 'node', 'deno', 'bun' ] as const // Command-specific timeouts (in milliseconds) const COMMAND_TIMEOUTS: Record<string, number> = { 'npm': 600000, // 10 minutes for npm install 'pnpm': 600000, // 10 minutes for pnpm install 'yarn': 600000, // 10 minutes for yarn install 'npx': 300000, // 5 minutes for npx commands 'git': 60000, // 1 minute for git operations 'tar': 120000, // 2 minutes for tar operations 'node': 300000, // 5 minutes for node execution 'deno': 300000, // 5 minutes for deno 'bun': 300000, // 5 minutes for bun 'default': 180000 // 3 minutes default } // Allowlist of npm/npx packages that can be executed const ALLOWED_NPX_PACKAGES = [ 'create-next-app', 'expo' ] as const // Arguments that should never be allowed (potential command injection) const FORBIDDEN_ARG_PATTERNS = [ /[;&|`$()]/, // Shell metacharacters /\n|\r/, // Line breaks /\0/, // Null bytes /\\.\\./, // Path traversal attempts ] as const // Allowed flags for package managers and other commands const ALLOWED_FLAGS = [ // NPM/PNPM/YARN flags '--force', '--legacy-peer-deps', '--silent', '--save-dev', '-D', '--dev', '--production', '--no-save', '--exact', '--save-exact', '--dry-run', '--verbose', '-v', '--version', '--help', '-h', // Git flags '--version', 'init', 'add', 'commit', '--message', '-m', '-A', // Tar flags '-xzf', '-C', '--strip-components', // NPX flags '--yes', '--no', '--quiet', '-q', '--package', '-p', // Create-next-app flags '--tailwind', '--eslint', '--typescript', '--app', '--src-dir', '--no-src-dir', '--no-import-alias', '--use-npm', '--use-pnpm', '--use-yarn', '--use-bun', '--turbopack', // Generic patterns for versioned flags and numeric options /^--use-[a-z]+$/, // --use-npm, --use-pnpm, etc. /^--strip-components=\d+$/, // --strip-components=1, etc. ] as const /** * Validates a command against the allowlist */ function validateCommand(command: string): void { if (!command || typeof command !== 'string') { throw new Error("Command must be a non-empty string") } if (!ALLOWED_COMMANDS.includes(command as any)) { throw new Error(`Command not allowed: ${command}. Allowed commands: ${ALLOWED_COMMANDS.join(', ')}`) } } /** * Validates command arguments for security issues */ function validateArguments(command: string, args: string[]): string[] { if (!Array.isArray(args)) { throw new Error("Arguments must be an array") } const validatedArgs: string[] = [] for (let i = 0; i < args.length; i++) { const arg = args[i] if (typeof arg !== 'string') { throw new Error(`Argument ${i} must be a string, got ${typeof arg}`) } // Check for forbidden patterns for (const pattern of FORBIDDEN_ARG_PATTERNS) { if (pattern.test(arg)) { throw new Error(`Dangerous pattern in argument ${i}: ${arg}`) } } // Check if argument starts with dash (potential flag) if (arg.startsWith('-')) { validateFlag(arg) } // Validate length (prevent DoS) if (arg.length > 1000) { throw new Error(`Argument ${i} too long: ${arg.length} characters`) } // Special validation for npx commands if (command === 'npx' && i === 0) { validateNpxPackage(arg) } validatedArgs.push(arg) } return validatedArgs } /** * Validates flags against the allowlist */ function validateFlag(flag: string): void { // Check against exact string matches const stringFlags = ALLOWED_FLAGS.filter(f => typeof f === 'string') as string[] if (stringFlags.includes(flag)) { return } // Check against regex patterns const regexFlags = ALLOWED_FLAGS.filter(f => f instanceof RegExp) as RegExp[] for (const pattern of regexFlags) { if (pattern.test(flag)) { return } } // Special cases for numeric arguments after flags (e.g., "--strip-components=1") const flagWithValue = flag.split('=')[0] if (stringFlags.includes(flagWithValue)) { return } throw new Error(`Flag not allowed: ${flag}. Use only approved flags.`) } /** * Validates npx package names against allowlist */ function validateNpxPackage(packageName: string): void { // Handle versioned packages (e.g., "create-next-app@latest") const baseName = packageName.split('@')[0] if (!ALLOWED_NPX_PACKAGES.includes(baseName as any)) { throw new Error(`NPX package not allowed: ${packageName}. Allowed packages: ${ALLOWED_NPX_PACKAGES.join(', ')}`) } // Additional validation for package versions if (packageName.includes('@')) { const version = packageName.split('@')[1] // Allow common version patterns: latest, canary, numbers, beta, rc const validVersionPattern = /^(latest|canary|\d+(\.\d+)*(-\w+)?|beta|rc(\d+)?)$/ if (!validVersionPattern.test(version)) { throw new Error(`Invalid package version: ${version}`) } } } /** * Securely executes a command with validation and safety measures */ export async function secureExeca( command: string, args: string[] = [], options: ExecaOptions = {} ): Promise<ExecaReturnValue> { // Validate command validateCommand(command) // Validate and sanitize arguments const sanitizedArgs = validateArguments(command, args) // Validate working directory if provided let safeCwd = options.cwd if (safeCwd) { safeCwd = validateWorkingDirectory(safeCwd as string) } // Set security-focused options const secureOptions: ExecaOptions = { ...options, cwd: safeCwd, timeout: COMMAND_TIMEOUTS[command] || COMMAND_TIMEOUTS.default, cleanup: true, killSignal: 'SIGTERM', windowsHide: true, // Prevent shell execution to avoid injection shell: false, // Limit environment variables env: { ...options.env, // Remove potentially dangerous env vars PATH: process.env.PATH, HOME: process.env.HOME, USER: process.env.USER, NODE_ENV: process.env.NODE_ENV } } try { return await execa(command, sanitizedArgs, secureOptions) } catch (error) { // Log security-relevant failures but don't expose internal details if (error instanceof Error) { if (error.message.includes('ENOENT')) { throw new Error(`Command not found: ${command}`) } if (error.message.includes('timeout')) { throw new Error(`Command timed out: ${command}`) } if (error.message.includes('EACCES')) { throw new Error(`Permission denied: ${command}`) } } // Re-throw the error but ensure we don't leak sensitive information throw new Error(`Command execution failed: ${command}`) } } /** * Wrapper for npm install operations with additional validation */ export async function secureNpmInstall( packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun', dependencies: string[], devDependencies: string[], cwd: string, flags: string[] = [] ): Promise<void> { // Validate package names const allPackages = [...dependencies, ...devDependencies] for (const pkg of allPackages) { validatePackageName(pkg) } // Validate flags using the central validation function for (const flag of flags) { if (flag.startsWith('-')) { validateFlag(flag) } } // Execute package installation based on package manager if (packageManager === 'npm') { if (dependencies.length > 0) { await secureExeca('npm', ['install', '--legacy-peer-deps', ...flags, ...dependencies], { cwd }) } if (devDependencies.length > 0) { await secureExeca('npm', ['install', '--legacy-peer-deps', ...flags, '-D', ...devDependencies], { cwd }) } } else if (packageManager === 'pnpm') { if (dependencies.length > 0) { await secureExeca('pnpm', ['install', ...flags, ...dependencies], { cwd }) } if (devDependencies.length > 0) { await secureExeca('pnpm', ['install', ...flags, '-D', ...devDependencies], { cwd }) } } else { throw new Error(`Package manager not fully supported yet: ${packageManager}`) } } /** * Validates package names to prevent malicious packages */ function validatePackageName(packageName: string): void { if (!packageName || typeof packageName !== 'string') { throw new Error("Package name must be a non-empty string") } // Basic npm package name validation const validPackagePattern = /^(@[a-z0-9-_]+\/)?[a-z0-9-_.]+(@[a-z0-9-_.]+)?$/i if (!validPackagePattern.test(packageName)) { throw new Error(`Invalid package name format: ${packageName}`) } // Prevent excessively long package names if (packageName.length > 214) { throw new Error(`Package name too long: ${packageName}`) } // Prevent certain dangerous patterns const dangerousPatterns = [ /\.\./, // Path traversal /[;&|`$()]/, // Shell metacharacters /\s/, // Whitespace /\0/ // Null bytes ] for (const pattern of dangerousPatterns) { if (pattern.test(packageName)) { throw new Error(`Dangerous pattern in package name: ${packageName}`) } } }

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/reuvenaor/shadcn-registry-manager'

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