diff --git a/.github/workflows/integration-per-language.yml b/.github/workflows/integration-per-language.yml index 0a58d8e7..6e7623e0 100644 --- a/.github/workflows/integration-per-language.yml +++ b/.github/workflows/integration-per-language.yml @@ -44,12 +44,12 @@ jobs: --skip-file-detection \ --deploy-type helm \ --variable PORT=8080 \ - --variable APPNAME=testingCreateCommand \ + --variable APPNAME=testing-create-command \ --variable VERSION=1.11 \ --variable BUILDERVERSION=1.11 \ --variable SERVICEPORT=8080 \ - --variable NAMESPACE=testNamespace \ - --variable IMAGENAME=testImage \ + --variable NAMESPACE=test-namespace \ + --variable IMAGENAME=testimage \ --variable IMAGETAG=latest \ --variable ENTRYPOINT=myapp.py - name: Validate JSON @@ -150,7 +150,7 @@ jobs: --variable BRANCHNAME=main \ --variable ACRRESOURCEGROUP=someAcrResourceGroup \ --variable AZURECONTAINERREGISTRY=someRegistry \ - --variable CONTAINERNAME=someContainer \ + --variable CONTAINERNAME=somecontainer \ --variable CLUSTERRESOURCEGROUP=someClusterResourceGroup \ --variable CLUSTERNAME=someAksCluster \ --variable DOCKERFILE=./Dockerfile \ @@ -216,12 +216,12 @@ jobs: --skip-file-detection \ --deploy-type kustomize \ --variable PORT=8080 \ - --variable APPNAME=testingCreateCommand \ + --variable APPNAME=testing-create-command \ --variable VERSION=1.11 \ --variable BUILDERVERSION=1.11 \ --variable SERVICEPORT=8080 \ - --variable NAMESPACE=testNamespace \ - --variable IMAGENAME=testImage \ + --variable NAMESPACE=test-namespace \ + --variable IMAGENAME=testimage \ --variable IMAGETAG=latest \ --variable ENTRYPOINT=myapp.py - name: Validate JSON @@ -315,7 +315,7 @@ jobs: --variable BRANCHNAME=main \ --variable ACRRESOURCEGROUP=someAcrResourceGroup \ --variable AZURECONTAINERREGISTRY=someRegistry \ - --variable CONTAINERNAME=someContainer \ + --variable CONTAINERNAME=somecontainer \ --variable CLUSTERRESOURCEGROUP=someClusterResourceGroup \ --variable CLUSTERNAME=someAksCluster \ --variable DOCKERFILE=./Dockerfile \ @@ -380,12 +380,12 @@ jobs: --skip-file-detection \ --deploy-type manifests \ --variable PORT=8080 \ - --variable APPNAME=testingCreateCommand \ + --variable APPNAME=testing-create-command \ --variable VERSION=1.11 \ --variable BUILDERVERSION=1.11 \ --variable SERVICEPORT=80 \ - --variable NAMESPACE=testNamespace \ - --variable IMAGENAME=testImage \ + --variable NAMESPACE=test-namespace \ + --variable IMAGENAME=testimage \ --variable IMAGETAG=latest \ --variable ENTRYPOINT=myapp.py - name: Validate JSON @@ -472,7 +472,7 @@ jobs: --variable BRANCHNAME=main \ --variable ACRRESOURCEGROUP=someAcrResourceGroup \ --variable AZURECONTAINERREGISTRY=someRegistry \ - --variable CONTAINERNAME=someContainer \ + --variable CONTAINERNAME=somecontainer \ --variable CLUSTERRESOURCEGROUP=someClusterResourceGroup \ --variable CLUSTERNAME=someAksCluster \ --variable DOCKERFILE=./Dockerfile \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6ab8cb..a52217f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [0.17.16] - 2026-10-12 + +- Security: validate and encode template variables to prevent instruction injection in generated artifacts ([GHSA-xqmq-h7rh-g4cq](https://github.com/Azure/draft/security/advisories/GHSA-xqmq-h7rh-g4cq)) + ## [0.17.14] - 2026-01-08 - Dependabot updates for dependencies and actions diff --git a/cmd/create_test.go b/cmd/create_test.go index 4ea50928..dd02d000 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -34,7 +34,7 @@ func TestRun(t *testing.T) { {Name: "DOCKERFILENAME", Value: "Dockerfile"}, }, } - flagVariablesMap = map[string]string{"PORT": "8080", "APPNAME": "testing-create-command", "VERSION": "1.18", "SERVICEPORT": "8080", "NAMESPACE": "test-namespace", "IMAGENAME": "testImage", "IMAGETAG": "latest", "DOCKERFILENAME": "test.Dockerfile"} + flagVariablesMap = map[string]string{"PORT": "8080", "APPNAME": "testing-create-command", "VERSION": "1.18", "SERVICEPORT": "8080", "NAMESPACE": "test-namespace", "IMAGENAME": "host.minikube.internal:5001/testapp", "IMAGETAG": "latest", "DOCKERFILENAME": "test.Dockerfile"} mockCC := createCmd{ deployType: deployType, dest: testDir, diff --git a/pkg/config/validators/validators.go b/pkg/config/validators/validators.go index e5649965..cee76738 100644 --- a/pkg/config/validators/validators.go +++ b/pkg/config/validators/validators.go @@ -22,10 +22,18 @@ const ( maxImageTagLength = 128 // maxPort is the highest valid TCP/UDP port. maxPort = 65535 + + // Docker reference name grammar (namePat), without tags or digests: + // https://github.com/distribution/reference/blob/main/regexp.go + imageDomainComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` + imageHost = `(?:` + imageDomainComponent + `(?:\.` + imageDomainComponent + `)*|\[(?:[a-fA-F0-9:]+)\])` + imagePathComponent = `[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*` + imageName = `(?:` + imageHost + `(?::[0-9]+)?/)?` + imagePathComponent + `(?:/` + imagePathComponent + `)*` ) var ( ciNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*$`) + containerImageNamePattern = regexp.MustCompile(`^` + imageName + `$`) helmChartOverridesPattern = regexp.MustCompile(`^[A-Za-z0-9_.-]+=[A-Za-z0-9_.:/+-]+(?:,[A-Za-z0-9_.-]+=[A-Za-z0-9_.:/+-]+)*$`) ) @@ -50,8 +58,10 @@ func safeNameRunes(r rune) bool { func GetValidator(variableKind string) func(string) error { switch variableKind { - case "azureContainerRegistry", "azureManagedCluster", "azureResourceGroup", "containerImageName": + case "azureContainerRegistry", "azureManagedCluster", "azureResourceGroup": return ciNameValidator + case "containerImageName": + return containerImageNameValidator case "containerImageVersion": return containerImageVersionValidator case "dirPath": @@ -90,6 +100,15 @@ func ciNameValidator(input string) error { return nil } +// Image tags are supplied separately by containerImageVersion. Anchor Docker's +// name grammar so a valid substring cannot conceal trailing shell syntax. +func containerImageNameValidator(input string) error { + if !containerImageNamePattern.MatchString(input) { + return fmt.Errorf("invalid container image name %q: expected a repository name with an optional registry and port", input) + } + return nil +} + func kubernetesDNSLabelValidator(input string) error { if errs := validation.IsDNS1123Label(input); len(errs) > 0 { return fmt.Errorf("invalid Kubernetes name %q: %s", input, strings.Join(errs, "; ")) diff --git a/pkg/config/validators/validators_test.go b/pkg/config/validators/validators_test.go index f97b657c..fd12f4dd 100644 --- a/pkg/config/validators/validators_test.go +++ b/pkg/config/validators/validators_test.go @@ -203,6 +203,23 @@ func TestCINameValidatorsRejectShellSyntax(t *testing.T) { assert.Nil(t, GetValidator("kubernetesNamespace")("team-a")) } +func TestContainerImageNameValidator(t *testing.T) { + for _, input := range []string{ + "testapp", "team/my_app", "mcr.microsoft.com/team/app", + "host.minikube.internal:5001/testapp", "localhost:5000/team/app", + "127.0.0.1:5000/app", + } { + assert.NoError(t, GetValidator("containerImageName")(input), "expected %q to be valid", input) + } + for _, input := range []string{ + "", "app:latest", "app@sha256:abcd", "https://registry/app", "UPPERCASE", + "registry:port/app", "app;command", "app$(command)", "app --option value", + "app\nRUN command", `app"`, "app#comment", + } { + assert.Error(t, GetValidator("containerImageName")(input), "expected %q to be rejected", input) + } +} + func TestKubernetesResourceNameValidator(t *testing.T) { for _, input := range []string{"app", "my-app", "app2"} { assert.Nil(t, GetValidator("kubernetesResourceName")(input), "expected %q to be valid", input) diff --git a/pkg/fixtures/workflows/github/helm/.github/workflows/azure-kubernetes-service-helm.yml b/pkg/fixtures/workflows/github/helm/.github/workflows/azure-kubernetes-service-helm.yml index 724e6354..12bae8df 100644 --- a/pkg/fixtures/workflows/github/helm/.github/workflows/azure-kubernetes-service-helm.yml +++ b/pkg/fixtures/workflows/github/helm/.github/workflows/azure-kubernetes-service-helm.yml @@ -44,7 +44,7 @@ on: env: ACR_RESOURCE_GROUP: testAcrRG AZURE_CONTAINER_REGISTRY: testAcr - CONTAINER_NAME: testContainer + CONTAINER_NAME: testcontainer CLUSTER_NAME: testCluster CLUSTER_RESOURCE_GROUP: testClusterRG CLUSTER_RESOURCE_TYPE: Microsoft.ContainerService/managedClusters diff --git a/pkg/fixtures/workflows/github/kustomize/.github/workflows/azure-kubernetes-service-kustomize.yml b/pkg/fixtures/workflows/github/kustomize/.github/workflows/azure-kubernetes-service-kustomize.yml index 48c873c6..9fbd35cb 100644 --- a/pkg/fixtures/workflows/github/kustomize/.github/workflows/azure-kubernetes-service-kustomize.yml +++ b/pkg/fixtures/workflows/github/kustomize/.github/workflows/azure-kubernetes-service-kustomize.yml @@ -45,7 +45,7 @@ on: env: ACR_RESOURCE_GROUP: testAcrRG AZURE_CONTAINER_REGISTRY: testAcr - CONTAINER_NAME: testContainer + CONTAINER_NAME: testcontainer CLUSTER_NAME: testCluster CLUSTER_RESOURCE_GROUP: testClusterRG CLUSTER_RESOURCE_TYPE: Microsoft.ContainerService/managedClusters diff --git a/pkg/handlers/templatetests/workflows_github_helm_test.go b/pkg/handlers/templatetests/workflows_github_helm_test.go index 52bf5b4c..48682b9c 100644 --- a/pkg/handlers/templatetests/workflows_github_helm_test.go +++ b/pkg/handlers/templatetests/workflows_github_helm_test.go @@ -20,7 +20,7 @@ func TestGitHubWorkflowHelmTemplates(t *testing.T) { "BRANCHNAME": "testBranch", "ACRRESOURCEGROUP": "testAcrRG", "AZURECONTAINERREGISTRY": "testAcr", - "CONTAINERNAME": "testContainer", + "CONTAINERNAME": "testcontainer", "CLUSTERRESOURCEGROUP": "testClusterRG", "CLUSTERRESOURCETYPE": "Microsoft.ContainerService/managedClusters", "CLUSTERNAME": "testCluster", diff --git a/pkg/handlers/templatetests/workflows_github_kustomize_test.go b/pkg/handlers/templatetests/workflows_github_kustomize_test.go index efbf7231..359a4541 100644 --- a/pkg/handlers/templatetests/workflows_github_kustomize_test.go +++ b/pkg/handlers/templatetests/workflows_github_kustomize_test.go @@ -20,7 +20,7 @@ func TestGitHubWorkflowKustomizeTemplates(t *testing.T) { "BRANCHNAME": "testBranch", "ACRRESOURCEGROUP": "testAcrRG", "AZURECONTAINERREGISTRY": "testAcr", - "CONTAINERNAME": "testContainer", + "CONTAINERNAME": "testcontainer", "CLUSTERRESOURCEGROUP": "testClusterRG", "CLUSTERRESOURCETYPE": "Microsoft.ContainerService/managedClusters", "CLUSTERNAME": "testCluster",