diff --git a/internal/builder/config.go b/internal/builder/config.go index bd3d87a..56e6b12 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -3,10 +3,23 @@ package builder import ( "fmt" "os" + "regexp" + "strings" "gopkg.in/yaml.v3" ) +// repoNameRegex validates repository names for safe use in NATS subjects. +// Only allows alphanumeric, dashes, and underscores (no dots or wildcards). +var repoNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +// validURLPrefixes are the allowed prefixes for repository URLs. +var validURLPrefixes = []string{ + "git+https://", + "git+ssh://", + "git+file://", +} + // RepoConfig holds configuration for a single repository. type RepoConfig struct { URL string `yaml:"url"` @@ -44,9 +57,27 @@ func (c *Config) Validate() error { } for name, repo := range c.Repos { + // Validate repo name for safe use in NATS subjects + if !repoNameRegex.MatchString(name) { + return fmt.Errorf("repo name %q contains invalid characters (only alphanumeric, dash, underscore allowed)", name) + } + if repo.URL == "" { return fmt.Errorf("repo %q: url is required", name) } + + // Validate URL format + validURL := false + for _, prefix := range validURLPrefixes { + if strings.HasPrefix(repo.URL, prefix) { + validURL = true + break + } + } + if !validURL { + return fmt.Errorf("repo %q: url must start with git+https://, git+ssh://, or git+file://", name) + } + if repo.DefaultBranch == "" { return fmt.Errorf("repo %q: default_branch is required", name) } diff --git a/internal/messages/build.go b/internal/messages/build.go index 82749a2..da310df 100644 --- a/internal/messages/build.go +++ b/internal/messages/build.go @@ -3,6 +3,7 @@ package messages import ( "encoding/json" "fmt" + "strings" ) // BuildStatus represents the status of a build response. @@ -55,6 +56,10 @@ func (r *BuildRequest) Validate() error { if r.ReplyTo == "" { return fmt.Errorf("reply_to is required") } + // Validate reply_to format to prevent publishing to arbitrary subjects + if !strings.HasPrefix(r.ReplyTo, "build.responses.") { + return fmt.Errorf("invalid reply_to format: must start with 'build.responses.'") + } return nil }