package handlers import ( "errors" "net/url" "regexp" "strings" ) // Repo URL validation errors. var ( errRepoURLEmpty = errors.New("repository URL must not be empty") errRepoURLScheme = errors.New("file:// URLs are not allowed for security reasons") errRepoURLInvalid = errors.New("repository URL must use https://, http://, ssh://, git://, or git@host:path format") errRepoURLNoHost = errors.New("repository URL must include a host") errRepoURLNoPath = errors.New("repository URL must include a path") ) // scpLikeRepoRe matches SCP-like git URLs: git@host:path (e.g. git@github.com:user/repo.git). // Only the "git" user is allowed, as that is the standard for SSH deploy keys. var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`) // ValidateRepoURL checks that the given repository URL is valid and uses an allowed scheme. func ValidateRepoURL(repoURL string) error { if strings.TrimSpace(repoURL) == "" { return errRepoURLEmpty } // Reject path traversal in any URL format if strings.Contains(repoURL, "..") { return errRepoURLInvalid } // Check for SCP-like git URLs first (git@host:path) if scpLikeRepoRe.MatchString(repoURL) { return nil } // Reject file:// explicitly if strings.HasPrefix(strings.ToLower(repoURL), "file://") { return errRepoURLScheme } return validateParsedURL(repoURL) } // validateParsedURL validates a standard URL format repository URL. func validateParsedURL(repoURL string) error { parsed, err := url.Parse(repoURL) if err != nil { return errRepoURLInvalid } switch strings.ToLower(parsed.Scheme) { case "https", "http", "ssh", "git": // allowed default: return errRepoURLInvalid } if parsed.Host == "" { return errRepoURLNoHost } if parsed.Path == "" || parsed.Path == "/" { return errRepoURLNoPath } return nil }