Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pkg/provisioner/templates/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ var (
// gitRefPattern matches safe git references (branches, tags, SHAs, PR refs).
// Allows "/" for refs like "refs/tags/v1.31.0" or "refs/pull/123/head".
gitRefPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.\-+~/]*$`)

// featureGatePattern matches valid Kubernetes feature gates like "FeatureName=true".
featureGatePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*=(true|false)$`)

// hostnamePattern matches safe hostnames and IP addresses.
hostnamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.\-:]*$`)
)

// ValidateTemplateInputs validates user-supplied fields that will be interpolated
Expand Down Expand Up @@ -114,6 +120,32 @@ func ValidateTemplateInputs(env v1alpha1.Environment) error {
}
}

// Validate track branches (same rules as git refs)
if env.Spec.Kubernetes.Latest != nil && env.Spec.Kubernetes.Latest.Track != "" {
if !gitRefPattern.MatchString(env.Spec.Kubernetes.Latest.Track) {
return fmt.Errorf("invalid kubernetes latest track branch: %q contains disallowed characters", env.Spec.Kubernetes.Latest.Track)
}
}
if env.Spec.NVIDIAContainerToolkit.Latest != nil && env.Spec.NVIDIAContainerToolkit.Latest.Track != "" {
if !gitRefPattern.MatchString(env.Spec.NVIDIAContainerToolkit.Latest.Track) {
return fmt.Errorf("invalid nvidia container toolkit latest track branch: %q contains disallowed characters", env.Spec.NVIDIAContainerToolkit.Latest.Track)
}
}

// Validate feature gates
for _, gate := range env.Spec.Kubernetes.K8sFeatureGates {
if !featureGatePattern.MatchString(gate) {
return fmt.Errorf("invalid kubernetes feature gate: %q must match FeatureName=true|false", gate)
}
}

// Validate endpoint host
if env.Spec.Kubernetes.K8sEndpointHost != "" {
if !hostnamePattern.MatchString(env.Spec.Kubernetes.K8sEndpointHost) {
return fmt.Errorf("invalid kubernetes endpoint host: %q contains disallowed characters", env.Spec.Kubernetes.K8sEndpointHost)
}
Comment on lines 45 to 146
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hostnamePattern currently allows : and the hostname tests accept values like k8s-api:6443, but the Kubernetes templates append :6443 when using K8sEndpointHost (e.g., --control-plane-endpoint="${K8S_ENDPOINT_HOST}:6443" and controlPlaneEndpoint: "{{.ControlPlaneEndpoint}}:6443"). This combination can produce invalid endpoints like k8s-api:6443:6443 if a user includes a port. Consider either tightening validation to reject host:port (no : allowed), or updating the templates/templating data model to treat this field as host[:port] and only append the default port when none is provided.

Copilot uses AI. Check for mistakes.
}

// Validate file paths
filePaths := map[string]string{
"private key path": env.Spec.PrivateKey,
Expand Down
117 changes: 117 additions & 0 deletions pkg/provisioner/templates/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,120 @@ func TestValidateTemplateInputs_Injection(t *testing.T) {
t.Error("expected error for injection attempt, got nil")
}
}

func TestFeatureGatePattern(t *testing.T) {
accept := []string{"FeatureName=true", "MyGate=false", "A=true"}
reject := []string{
"bad;rm -rf /",
"NoValue",
"=true",
"Feature=maybe",
"Feature=true; echo pwned",
"$(curl evil)=true",
}

for _, v := range accept {
if !featureGatePattern.MatchString(v) {
t.Errorf("featureGatePattern should accept %q", v)
}
}
for _, v := range reject {
if featureGatePattern.MatchString(v) {
t.Errorf("featureGatePattern should reject %q", v)
}
}
}

func TestHostnamePattern(t *testing.T) {
accept := []string{"host.example.com", "192.168.1.1", "k8s-api:6443", "my-host"}
reject := []string{
"host.com; rm -rf /",
"$(curl evil)",
"host && bad",
"host`id`",
"; echo pwned",
Comment on lines +156 to +162
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestHostnamePattern treats k8s-api:6443 as a valid K8sEndpointHost, but the kubeadm templates append :6443 to the value, so accepting host:port can lead to malformed endpoints (...:6443:6443). Adjust the accepted cases to match the intended contract (host-only) or update the implementation to support optional ports consistently.

Suggested change
accept := []string{"host.example.com", "192.168.1.1", "k8s-api:6443", "my-host"}
reject := []string{
"host.com; rm -rf /",
"$(curl evil)",
"host && bad",
"host`id`",
"; echo pwned",
accept := []string{"host.example.com", "192.168.1.1", "my-host"}
reject := []string{
"host.com; rm -rf /",
"$(curl evil)",
"host && bad",
"host`id`",
"; echo pwned",
"k8s-api:6443",

Copilot uses AI. Check for mistakes.
}

for _, v := range accept {
if !hostnamePattern.MatchString(v) {
t.Errorf("hostnamePattern should accept %q", v)
}
}
for _, v := range reject {
if hostnamePattern.MatchString(v) {
t.Errorf("hostnamePattern should reject %q", v)
}
}
}

func TestValidateTemplateInputs_RejectsShellInFeatureGates(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.K8sFeatureGates = []string{"Valid=true", "bad;rm -rf /"}
err := ValidateTemplateInputs(env)
if err == nil {
t.Error("expected error for shell injection in feature gate, got nil")
}
}

func TestValidateTemplateInputs_RejectsShellInTrackBranch(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.Latest = &v1alpha1.K8sLatestSpec{
Repo: "https://github.com/kubernetes/kubernetes",
Track: "main; curl evil.com",
}
err := ValidateTemplateInputs(env)
if err == nil {
t.Error("expected error for shell injection in track branch, got nil")
}
}

func TestValidateTemplateInputs_RejectsCTKTrackBranch(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.NVIDIAContainerToolkit.Latest = &v1alpha1.CTKLatestSpec{
Repo: "https://github.com/NVIDIA/nvidia-container-toolkit",
Track: "main && curl evil.com",
}
err := ValidateTemplateInputs(env)
if err == nil {
t.Error("expected error for shell injection in CTK track branch, got nil")
}
}

func TestValidateTemplateInputs_RejectsShellInEndpointHost(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.K8sEndpointHost = "host.com; rm -rf /"
err := ValidateTemplateInputs(env)
if err == nil {
t.Error("expected error for shell injection in endpoint host, got nil")
}
}

func TestValidateTemplateInputs_AcceptsValidFeatureGates(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.K8sFeatureGates = []string{"GracefulNodeShutdown=true", "TopologyManager=false"}
err := ValidateTemplateInputs(env)
if err != nil {
t.Errorf("expected no error for valid feature gates, got: %v", err)
}
}

func TestValidateTemplateInputs_AcceptsValidTrackBranch(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.Latest = &v1alpha1.K8sLatestSpec{
Repo: "https://github.com/kubernetes/kubernetes",
Track: "master",
}
err := ValidateTemplateInputs(env)
if err != nil {
t.Errorf("expected no error for valid track branch, got: %v", err)
}
}

func TestValidateTemplateInputs_AcceptsValidEndpointHost(t *testing.T) {
env := v1alpha1.Environment{}
env.Spec.Kubernetes.K8sEndpointHost = "k8s-api.example.com"
err := ValidateTemplateInputs(env)
if err != nil {
t.Errorf("expected no error for valid endpoint host, got: %v", err)
}
}
Loading