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._-]+:.+$`) // allowedRepoSchemes lists the URL schemes accepted for repository URLs. // //nolint:gochecknoglobals // package-level constant map parsed once var allowedRepoSchemes = map[string]bool{ "https": true, "http": true, "ssh": true, "git": true, } // 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 validateParsedRepoURL(repoURL) } // validateParsedRepoURL validates a standard URL-format repository URL. func validateParsedRepoURL(repoURL string) error { parsed, err := url.Parse(repoURL) if err != nil { return errRepoURLInvalid } if !allowedRepoSchemes[strings.ToLower(parsed.Scheme)] { return errRepoURLInvalid } if parsed.Host == "" { return errRepoURLNoHost } if parsed.Path == "" || parsed.Path == "/" { return errRepoURLNoPath } return nil }