From c52e88ca7e61991bee4b4abf8fb6a6484b3d947e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Tue, 10 Feb 2026 22:09:51 +0100 Subject: [PATCH] fix: add validation for config and reply subjects Address medium severity security issues: - Validate repo names in config only allow alphanumeric, dash, underscore (prevents NATS subject injection via dots or wildcards) - Validate repo URLs must start with git+https://, git+ssh://, or git+file:// - Validate ReplyTo field must start with "build.responses." to prevent publishing responses to arbitrary NATS subjects Co-Authored-By: Claude Opus 4.5 --- internal/builder/config.go | 31 +++++++++++++++++++++++++++++++ internal/messages/build.go | 5 +++++ 2 files changed, 36 insertions(+) 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 }