CI is red on `main` and on every open PR (including the Dependabot PRs #1531, #1552–#1554) with: ``` panic: file requires newer Go version go1.27 (application built with go1.26) golangci-lint exit with code 2 ``` ## Cause `actions/setup-go` with `go-version: 'stable'` started resolving to **go1.27.0**. The pinned golangci-lint **v2.11.4** release binary is built with **go1.26**, and its `go/types` refuses to typecheck source files declaring a newer language version. Nothing in the failing PRs is at fault. ## Fix Bump to **golangci-lint v2.13.1**, whose release binaries are built with go1.27, so linting works under `stable` again. That bump also pulls in newer linter versions, which surface 57 findings this tree did not have before. Handled as follows: | Linter | Count | Resolution | |---|---|---| | `goconst` | 50 | 39 were repeated literals in `_test.go` files — goconst started counting those in v2.12.0 (golangci-lint#6480 exposed `ignore-tests`). Fixture data like `"1.0.0"` (89 occurrences) is not a missing constant, so `goconst.ignore-tests: true`. The other 11 were real duplication in production code (OpenAPI tags, the `bearer` security scheme name, `$ref`, `http`/`https`) and became named constants. | | `gosec` G710 | 1 | **True positive, fixed.** See below. | | `staticcheck` SA1019 | 6 | Go 1.26 deprecated `ecdsa.PublicKey.X/Y` and `PrivateKey.D`. Annotated with the same targeted `nolint` + rationale already used for `ScalarBaseMult` in `cmd/publisher/auth/common.go`. | ### The open redirect is real `TrailingSlashMiddleware` copied the entire `*url.URL` before rewriting the path. For an absolute-form request URI — legal for a Go HTTP server to parse, e.g. `GET http://evil.com/foo/ HTTP/1.1` — `r.URL.Scheme` and `r.URL.Host` are populated, and they were echoed straight back in the `Location` header. The redirect target is now built from the cleaned path and query alone. The existing `path.Clean` hardening for GHSA-v8vw-gw5j-w7m6 (protocol-relative `//evil.com/`) is unchanged. ### Also Switched `gomodguard` → `gomodguard_v2`; the former is deprecated as of v2.12.0 and was emitting a warning on every run. ## Verification - `golangci-lint v2.13.1` against this tree: **0 issues**, no deprecation warnings - `make validate`: all validations passed - `go build ./...` and tests for every touched package pass (DB-backed tests need Postgres; left to CI) ## Note on migrating the ecdsa key paths Moving those six sites to `PublicKey.Bytes` / `ParseUncompressedPublicKey` / `crypto/ecdh` touches key parsing and signature verification. It should be its own reviewed PR rather than riding along on a CI unblock. Happy to open one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
package validators
|
|
|
|
import (
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
schemeHTTP = "http"
|
|
schemeHTTPS = "https"
|
|
)
|
|
|
|
var (
|
|
// Regular expressions for validating repository URLs
|
|
// These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository
|
|
// For example: // - GitHub: https://github.com/user/repo
|
|
githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`)
|
|
gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`)
|
|
)
|
|
|
|
// IsValidRepositoryURL checks if the given URL is valid for the specified repository source
|
|
func IsValidRepositoryURL(source RepositorySource, url string) bool {
|
|
switch source {
|
|
case SourceGitHub:
|
|
return githubURLRegex.MatchString(url)
|
|
case SourceGitLab:
|
|
return gitlabURLRegex.MatchString(url)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasNoSpaces checks if a string contains no spaces
|
|
func HasNoSpaces(s string) bool {
|
|
return !strings.Contains(s, " ")
|
|
}
|
|
|
|
// extractTemplateVariables extracts template variables from a URL string
|
|
// e.g., "http://{host}:{port}/mcp" returns ["host", "port"]
|
|
func extractTemplateVariables(url string) []string {
|
|
re := regexp.MustCompile(`\{([^}]+)\}`)
|
|
matches := re.FindAllStringSubmatch(url, -1)
|
|
|
|
var variables []string
|
|
for _, match := range matches {
|
|
if len(match) > 1 {
|
|
variables = append(variables, match[1])
|
|
}
|
|
}
|
|
return variables
|
|
}
|
|
|
|
// replaceTemplateVariables replaces template variables with placeholder values for URL validation
|
|
func replaceTemplateVariables(rawURL string) string {
|
|
// Replace common template variables with valid placeholder values for parsing
|
|
templateReplacements := map[string]string{
|
|
"{host}": "example.com",
|
|
"{port}": "8080",
|
|
"{path}": "api",
|
|
"{protocol}": schemeHTTP,
|
|
"{scheme}": schemeHTTP,
|
|
}
|
|
|
|
result := rawURL
|
|
for placeholder, replacement := range templateReplacements {
|
|
result = strings.ReplaceAll(result, placeholder, replacement)
|
|
}
|
|
|
|
// Handle any remaining {variable} patterns with context-appropriate placeholders
|
|
// If the variable is in a port position (after a colon in the host), use a numeric placeholder
|
|
// Pattern: :/{variable} or :{variable}/ or :{variable} at end
|
|
portRe := regexp.MustCompile(`:(\{[^}]+\})(/|$)`)
|
|
result = portRe.ReplaceAllString(result, ":8080$2")
|
|
|
|
// Replace any other remaining {variable} patterns with generic placeholder
|
|
re := regexp.MustCompile(`\{[^}]+\}`)
|
|
result = re.ReplaceAllString(result, "placeholder")
|
|
|
|
return result
|
|
}
|
|
|
|
// IsValidURL checks if a URL is in valid format (basic structure validation)
|
|
func IsValidURL(rawURL string) bool {
|
|
// Replace template variables with placeholders for parsing
|
|
testURL := replaceTemplateVariables(rawURL)
|
|
|
|
// Parse the URL
|
|
u, err := url.Parse(testURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Check if scheme is present (http or https)
|
|
if u.Scheme != schemeHTTP && u.Scheme != schemeHTTPS {
|
|
return false
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsValidSubfolderPath checks if a subfolder path is valid
|
|
func IsValidSubfolderPath(path string) bool {
|
|
// Empty path is valid (subfolder is optional)
|
|
if path == "" {
|
|
return true
|
|
}
|
|
|
|
// Must not start with / (must be relative)
|
|
if strings.HasPrefix(path, "/") {
|
|
return false
|
|
}
|
|
|
|
// Must not end with / (clean path format)
|
|
if strings.HasSuffix(path, "/") {
|
|
return false
|
|
}
|
|
|
|
// Check for valid path characters (alphanumeric, dash, underscore, dot, forward slash)
|
|
validPathRegex := regexp.MustCompile(`^[a-zA-Z0-9\-_./]+$`)
|
|
if !validPathRegex.MatchString(path) {
|
|
return false
|
|
}
|
|
|
|
// Check that path segments are valid
|
|
segments := strings.Split(path, "/")
|
|
for _, segment := range segments {
|
|
// Disallow empty segments ("//"), current dir ("."), and parent dir ("..")
|
|
if segment == "" || segment == "." || segment == ".." {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsValidRemoteURL checks if a URL is valid for remotes (stricter than packages - no localhost allowed)
|
|
func IsValidRemoteURL(rawURL string) bool {
|
|
// First check basic URL structure
|
|
if !IsValidURL(rawURL) {
|
|
return false
|
|
}
|
|
|
|
// Replace template variables with placeholders before parsing for localhost check
|
|
testURL := replaceTemplateVariables(rawURL)
|
|
|
|
// Parse the URL to check for localhost restriction
|
|
u, err := url.Parse(testURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Reject localhost URLs for remotes (security/production concerns)
|
|
hostname := u.Hostname()
|
|
if hostname == "localhost" || hostname == "127.0.0.1" || strings.HasSuffix(hostname, ".localhost") {
|
|
return false
|
|
}
|
|
|
|
if u.Scheme != schemeHTTPS {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsValidTemplatedURL validates a URL with template variables against available variables
|
|
// For packages: validates that template variables reference package arguments or environment variables
|
|
// For remotes: validates that template variables reference the transport's variables map
|
|
func IsValidTemplatedURL(rawURL string, availableVariables []string) bool {
|
|
// First check basic URL structure
|
|
if !IsValidURL(rawURL) {
|
|
return false
|
|
}
|
|
|
|
// Extract template variables from URL
|
|
templateVars := extractTemplateVariables(rawURL)
|
|
|
|
// If no templates are found, it's a valid static URL
|
|
if len(templateVars) == 0 {
|
|
return true
|
|
}
|
|
|
|
// Validate that all template variables are available
|
|
availableSet := make(map[string]bool)
|
|
for _, v := range availableVariables {
|
|
availableSet[v] = true
|
|
}
|
|
|
|
for _, templateVar := range templateVars {
|
|
if !availableSet[templateVar] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|