/** * Git Input Validation Utility * * Provides strict validation for git references (branch names, repository URLs) * to prevent command injection vulnerabilities. * * Security: Whitelist-based approach - only allow known-safe characters. */ import { BadRequestException, Logger } from "@nestjs/common"; const logger = new Logger("GitValidation"); /** * Validates a git branch name for safety * * Allowed format: alphanumeric, hyphens, underscores, forward slashes * Examples: "main", "feature/add-login", "fix/bug_123" * * Rejected: Special characters that could be interpreted as git syntax * Examples: "--option", "$(command)", ";malicious", "`command`" * * @param branchName - The branch name to validate * @throws BadRequestException if branch name is invalid */ export function validateBranchName(branchName: string): void { // Check for empty or whitespace-only if (!branchName || branchName.trim().length === 0) { throw new BadRequestException("Branch name cannot be empty"); } // Check length (git has a 255 char limit for ref names) if (branchName.length > 255) { throw new BadRequestException("Branch name exceeds maximum length (255 characters)"); } // Whitelist: only allow alphanumeric, hyphens, underscores, forward slashes, dots // This prevents all forms of command injection const safePattern = /^[a-zA-Z0-9/_.-]+$/; if (!safePattern.test(branchName)) { logger.warn({ event: "GIT_COMMAND_INJECTION_BLOCKED", input: branchName, reason: "Invalid characters detected", securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException( `Branch name contains invalid characters. Only alphanumeric, hyphens, underscores, slashes, and dots are allowed.` ); } // Prevent git option injection (branch names starting with -) if (branchName.startsWith("-")) { logger.warn({ event: "GIT_OPTION_INJECTION_BLOCKED", input: branchName, reason: "Branch name starts with hyphen (option injection attempt)", securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException( "Branch name cannot start with a hyphen (prevents option injection)" ); } // Prevent double dots (used for range specifications in git) if (branchName.includes("..")) { logger.warn({ event: "GIT_RANGE_INJECTION_BLOCKED", input: branchName, reason: "Double dots detected (git range specification)", securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException("Branch name cannot contain consecutive dots (..)"); } // Prevent path traversal patterns if (branchName.includes("/../") || branchName.startsWith("../") || branchName.endsWith("/..")) { logger.warn({ event: "GIT_PATH_TRAVERSAL_BLOCKED", input: branchName, reason: "Path traversal pattern detected", securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException("Branch name cannot contain path traversal patterns"); } // Prevent ending with .lock (reserved by git) if (branchName.endsWith(".lock")) { throw new BadRequestException("Branch name cannot end with .lock (reserved by git)"); } // Prevent control characters // eslint-disable-next-line no-control-regex if (/[\x00-\x1F\x7F]/.test(branchName)) { throw new BadRequestException("Branch name cannot contain control characters"); } } /** * Validates a git repository URL for safety * * Allowed protocols: https, http (dev only), ssh (git@) * Prevents: file://, javascript:, data:, and other dangerous protocols * * @param repositoryUrl - The repository URL to validate * @throws BadRequestException if URL is invalid or uses dangerous protocol */ export function validateRepositoryUrl(repositoryUrl: string): void { // Check for empty or whitespace-only if (!repositoryUrl || repositoryUrl.trim().length === 0) { throw new BadRequestException("Repository URL cannot be empty"); } // Check length (reasonable limit for URLs) if (repositoryUrl.length > 2000) { throw new BadRequestException("Repository URL exceeds maximum length (2000 characters)"); } // Remove whitespace const url = repositoryUrl.trim(); // Whitelist allowed protocols const httpsPattern = /^https:\/\//i; const httpPattern = /^http:\/\//i; // Only for development const sshGitPattern = /^git@[a-zA-Z0-9.-]+:/; // git@host:repo format const sshUrlPattern = /^ssh:\/\/git@[a-zA-Z0-9.-]+(\/|:)/; // ssh://git@host/repo or ssh://git@host:repo if ( !httpsPattern.test(url) && !httpPattern.test(url) && !sshGitPattern.test(url) && !sshUrlPattern.test(url) && !url.startsWith("git://") ) { throw new BadRequestException( "Repository URL must use https://, http://, ssh://, git://, or git@ protocol" ); } // Prevent dangerous protocols const dangerousProtocols = [ "file://", "javascript:", "data:", "vbscript:", "about:", "chrome:", "view-source:", ]; for (const dangerous of dangerousProtocols) { if (url.toLowerCase().startsWith(dangerous)) { logger.warn({ event: "GIT_DANGEROUS_PROTOCOL_BLOCKED", input: url, protocol: dangerous, reason: `Dangerous protocol detected: ${dangerous}`, securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException( `Repository URL cannot use ${dangerous} protocol (security risk)` ); } } // Prevent localhost/internal network access (SSRF protection) const localhostPatterns = [ /https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|::1)/i, /https?:\/\/192\.168\./i, /https?:\/\/10\./i, /https?:\/\/172\.(1[6-9]|2\d|3[01])\./i, ]; for (const pattern of localhostPatterns) { if (pattern.test(url)) { logger.warn({ event: "GIT_SSRF_ATTEMPT_BLOCKED", input: url, reason: "Repository URL points to localhost or internal network", securityEvent: true, timestamp: new Date().toISOString(), }); throw new BadRequestException( "Repository URL cannot point to localhost or internal networks (SSRF protection)" ); } } // Prevent credential injection in URL if (url.includes("@") && !sshGitPattern.test(url) && !sshUrlPattern.test(url)) { // Extract the part before @ to check if it looks like credentials const beforeAt = url.split("@")[0]; if (beforeAt.includes("://") && beforeAt.split("://")[1].includes(":")) { throw new BadRequestException("Repository URL cannot contain embedded credentials"); } } // Prevent control characters and dangerous characters in URL // eslint-disable-next-line no-control-regex if (/[\x00-\x1F\x7F`$;|&]/.test(url)) { throw new BadRequestException("Repository URL contains invalid or dangerous characters"); } } /** * Validates a complete agent spawn context * * @param context - The spawn context with repository and branch * @throws BadRequestException if any field is invalid */ export function validateSpawnContext(context: { repository: string; branch: string }): void { validateRepositoryUrl(context.repository); validateBranchName(context.branch); }