diff --git a/api/holodeck/v1alpha1/validation.go b/api/holodeck/v1alpha1/validation.go index f72140cd9..828eb95a2 100644 --- a/api/holodeck/v1alpha1/validation.go +++ b/api/holodeck/v1alpha1/validation.go @@ -18,8 +18,23 @@ package v1alpha1 import ( "fmt" + "regexp" ) +var k8sLabelPattern = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._\-/]*[a-zA-Z0-9])?$`) + +func validateLabels(labels map[string]string) error { + for k, v := range labels { + if !k8sLabelPattern.MatchString(k) { + return fmt.Errorf("invalid label key %q: contains disallowed characters", k) + } + if v != "" && !k8sLabelPattern.MatchString(v) { + return fmt.Errorf("invalid label value %q for key %q: contains disallowed characters", v, k) + } + } + return nil +} + // Validate validates the ClusterSpec configuration. func (c *ClusterSpec) Validate() error { if c == nil { @@ -43,6 +58,16 @@ func (c *ClusterSpec) Validate() error { } } + // Validate labels for shell-injection safety + if err := validateLabels(c.ControlPlane.Labels); err != nil { + return fmt.Errorf("control-plane labels: %w", err) + } + if c.Workers != nil { + if err := validateLabels(c.Workers.Labels); err != nil { + return fmt.Errorf("worker labels: %w", err) + } + } + // Validate HA configuration if c.HighAvailability != nil { if err := c.HighAvailability.Validate(c.ControlPlane.Count); err != nil { diff --git a/pkg/provisioner/cluster.go b/pkg/provisioner/cluster.go index d596a6672..7e66001e8 100644 --- a/pkg/provisioner/cluster.go +++ b/pkg/provisioner/cluster.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "io" + "net" "os" "strings" "sync" @@ -437,6 +438,13 @@ func (cp *ClusterProvisioner) configureNodes(firstCP NodeInfo, nodes []NodeInfo) } defer provisioner.Client.Close() // nolint: errcheck + // Validate all node IPs before interpolating into shell commands + for _, node := range nodes { + if net.ParseIP(node.PrivateIP) == nil { + return fmt.Errorf("invalid private IP for node %s: %q", node.Name, node.PrivateIP) + } + } + // Build the node configuration script // Note: Use sudo -E to preserve KUBECONFIG environment variable, or use --kubeconfig flag var script strings.Builder