fix: validate target and hostname inputs to prevent injection

Add input validation to address security concerns:

- Validate Target field in BuildRequest against safe character pattern
  (must be "all" or match alphanumeric/dash/underscore/dot pattern)
- Filter hostnames discovered from nix flake show output, skipping any
  with invalid characters before using them in build commands

This prevents potential command injection via crafted NATS messages or
malicious flake configurations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 22:07:26 +01:00
parent 14f5b31faf
commit 08f1fcc6ac
2 changed files with 19 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"regexp"
"sort" "sort"
"sync" "sync"
"time" "time"
@@ -13,6 +14,10 @@ import (
"git.t-juice.club/torjus/homelab-deploy/internal/nats" "git.t-juice.club/torjus/homelab-deploy/internal/nats"
) )
// hostnameRegex validates hostnames from flake output.
// Allows: alphanumeric, dashes, underscores, dots.
var hostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
// BuilderConfig holds the configuration for the builder. // BuilderConfig holds the configuration for the builder.
type BuilderConfig struct { type BuilderConfig struct {
NATSUrl string NATSUrl string
@@ -197,6 +202,16 @@ func (b *Builder) handleBuildRequest(subject string, data []byte) {
} }
return return
} }
// Filter out hostnames with invalid characters (security: prevent injection)
validHosts := make([]string, 0, len(hosts))
for _, host := range hosts {
if hostnameRegex.MatchString(host) {
validHosts = append(validHosts, host)
} else {
b.logger.Warn("skipping hostname with invalid characters", "hostname", host)
}
}
hosts = validHosts
// Sort hosts for consistent ordering // Sort hosts for consistent ordering
sort.Strings(hosts) sort.Strings(hosts)
} else { } else {

View File

@@ -45,6 +45,10 @@ func (r *BuildRequest) Validate() error {
if r.Target == "" { if r.Target == "" {
return fmt.Errorf("target is required") return fmt.Errorf("target is required")
} }
// Target must be "all" or a valid hostname (same format as revision/branch)
if r.Target != "all" && !revisionRegex.MatchString(r.Target) {
return fmt.Errorf("invalid target format: %q", r.Target)
}
if r.Branch != "" && !revisionRegex.MatchString(r.Branch) { if r.Branch != "" && !revisionRegex.MatchString(r.Branch) {
return fmt.Errorf("invalid branch format: %q", r.Branch) return fmt.Errorf("invalid branch format: %q", r.Branch)
} }