Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kargo-cli for openSUSE:Factory checked in at 2026-10-09 21:44:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kargo-cli (Old) and /work/SRC/openSUSE:Factory/.kargo-cli.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kargo-cli" Fri Oct 9 21:44:24 2026 rev:67 rq:1383652 version:1.12.3 Changes: -------- --- /work/SRC/openSUSE:Factory/kargo-cli/kargo-cli.changes 2026-10-08 11:59:37.419564571 +0200 +++ /work/SRC/openSUSE:Factory/.kargo-cli.new.3698736/kargo-cli.changes 2026-10-09 21:44:31.095133731 +0200 @@ -1,0 +2,6 @@ +Fri Oct 09 05:10:07 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.12.3: + No CLI-related changes or dependency updates + +------------------------------------------------------------------- Old: ---- kargo-cli-1.12.2.obscpio New: ---- kargo-cli-1.12.3.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kargo-cli.spec ++++++ --- /var/tmp/diff_new_pack.LdkaMY/_old 2026-10-09 21:44:35.260308999 +0200 +++ /var/tmp/diff_new_pack.LdkaMY/_new 2026-10-09 21:44:35.261309041 +0200 @@ -19,7 +19,7 @@ %define executable_name kargo Name: kargo-cli -Version: 1.12.2 +Version: 1.12.3 Release: 0 Summary: CLI for the Kubernetes application lifecycle orchestration License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.LdkaMY/_old 2026-10-09 21:44:35.292310346 +0200 +++ /var/tmp/diff_new_pack.LdkaMY/_new 2026-10-09 21:44:35.295310472 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/akuity/kargo.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v1.12.2</param> + <param name="revision">refs/tags/v1.12.3</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.LdkaMY/_old 2026-10-09 21:44:35.314311272 +0200 +++ /var/tmp/diff_new_pack.LdkaMY/_new 2026-10-09 21:44:35.317311398 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/akuity/kargo</param> <param name="changesrevision">17077e406ed309fcc830d08776d9b9a47fe6e134</param></service><service name="tar_scm"> <param name="url">https://github.com/akuity/kargo.git</param> - <param name="changesrevision">2b5f3a2f12c1eb6156253ba71d44818d4262c865</param></service></servicedata> + <param name="changesrevision">d544d7c6a9d4d99d096df52dcb8357375c26b729</param></service></servicedata> (No newline at EOF) ++++++ kargo-cli-1.12.2.obscpio -> kargo-cli-1.12.3.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_builder.go new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_builder.go --- old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_builder.go 2026-10-06 20:24:02.000000000 +0200 +++ new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_builder.go 2026-10-08 17:12:48.000000000 +0200 @@ -181,8 +181,20 @@ PluginConfig: buildPluginCfg, } + // Reject remote entries whose Git ref would be interpreted by git as an + // option. See kustomizeValidatorFS. + validatingFS := newKustomizeValidatorFS(kusFS) k := krusty.MakeKustomizer(buildOptions) - return k.Run(kusFS, path) + rm, err := k.Run(validatingFS, path) + if err != nil { + // Kustomize obscures validation errors (see kustomizeValidatorFS), so + // report them in preference to whatever error Kustomize returned. + if validationErr := validatingFS.Err(); validationErr != nil { + return nil, validationErr + } + return nil, err + } + return rm, nil } // fileNameFunc generates a filename for a resource. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_validator_fs.go new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_validator_fs.go --- old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_validator_fs.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_validator_fs.go 2026-10-08 17:12:48.000000000 +0200 @@ -0,0 +1,123 @@ +package builtin + +import ( + "fmt" + "maps" + "net/url" + "path/filepath" + "slices" + "strings" + + "sigs.k8s.io/kustomize/api/konfig" + kustypes "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +// kustomizeValidatorFS is a filesys.FileSystem that inspects every +// kustomization file read through it and rejects entries with a query +// parameter value that git could interpret as an option. +// +// Kustomize resolves remote Git references by running the system git binary +// as `git fetch --depth=1 <repository> <ref>`, passing the ref (taken from the +// entry's ?ref= or ?version= query parameter) as a literal argument without an +// end-of-options separator. A ref such as --upload-pack=<cmd> therefore causes +// git to execute an arbitrary command. +// +// Kustomize reads every kustomization file -- including those inside +// repositories it has cloned to resolve remote entries -- through the +// filesys.FileSystem passed to it, and it always reads and parses a +// kustomization file before resolving any of its entries. Validating at this +// layer therefore rejects a malicious ref before git is ever invoked, and also +// covers remote entries referenced transitively by other remote entries. See +// validateQueryValues for which values are rejected. +// +// Kustomize treats any error reading a kustomization file as the file being +// absent, which would obscure the reason for the failure. The first validation +// error is therefore also recorded, so that callers can report it via Err. +type kustomizeValidatorFS struct { + filesys.FileSystem + err error +} + +// newKustomizeValidatorFS wraps the given filesys.FileSystem so that +// kustomization files containing entries with unsafe query parameter values +// cannot be read through it. +func newKustomizeValidatorFS(fs filesys.FileSystem) *kustomizeValidatorFS { + return &kustomizeValidatorFS{FileSystem: fs} +} + +// Err returns the first validation error encountered by ReadFile, if any. +func (r *kustomizeValidatorFS) Err() error { + return r.err +} + +// ReadFile implements filesys.FileSystem. +func (r *kustomizeValidatorFS) ReadFile(path string) ([]byte, error) { + content, err := r.FileSystem.ReadFile(path) + if err != nil { + return nil, err + } + if !slices.Contains( + konfig.RecognizedKustomizationFileNames(), + filepath.Base(path), + ) { + return content, nil + } + // Parse the kustomization exactly as Kustomize does. If it cannot be + // parsed, Kustomize will fail to parse it as well and none of its entries + // will be resolved, so it is safe to return the content as-is and let + // Kustomize report the error. + var k kustypes.Kustomization + if err = k.Unmarshal(content); err != nil { + return content, nil + } + // Merges deprecated fields (e.g. bases) into their replacements. + k.FixKustomization() + for _, entries := range [][]string{ + k.Resources, + k.Components, + k.Generators, + k.Transformers, + k.Validators, + } { + for _, entry := range entries { + if err = validateQueryValues(entry); err != nil { + err = fmt.Errorf("error validating %s: %w", path, err) + if r.err == nil { + r.err = err + } + return nil, err + } + } + } + return content, nil +} + +// validateQueryValues returns an error if the value of any query parameter of +// the given kustomization entry begins with "-". +// +// Kustomize currently passes only the ref (taken from the ref or version query +// parameter) to git, and a valid Git ref never begins with "-". Every +// parameter is checked nonetheless, so that a parameter Kustomize starts +// passing to git in the future cannot silently reintroduce option injection. +func validateQueryValues(entry string) error { + _, query, ok := strings.Cut(entry, "?") + if !ok { + return nil + } + // A query that cannot be fully parsed still yields every parameter that + // could be parsed. Those are checked too, rather than relying on Kustomize + // ignoring malformed queries. + values, _ := url.ParseQuery(query) + for _, param := range slices.Sorted(maps.Keys(values)) { + for _, value := range values[param] { + if strings.HasPrefix(value, "-") { + return fmt.Errorf( + "entry %q specifies query parameter %s %q, which must not begin with %q", + entry, param, value, "-", + ) + } + } + } + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_validator_fs_test.go new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_validator_fs_test.go --- old/kargo-cli-1.12.2/pkg/promotion/runner/builtin/kustomize_validator_fs_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kargo-cli-1.12.3/pkg/promotion/runner/builtin/kustomize_validator_fs_test.go 2026-10-08 17:12:48.000000000 +0200 @@ -0,0 +1,338 @@ +package builtin + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + securefs "github.com/fluxcd/pkg/kustomize/filesys" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +func Test_validateQueryValues(t *testing.T) { + testCases := []struct { + name string + entry string + assert func(*testing.T, error) + }{ + { + name: "local path", + entry: "../base", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "remote without query", + entry: "https://github.com/example/repo//base", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "remote with safe ref", + entry: "https://github.com/example/repo//base?ref=main", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "remote with safe version and other params", + entry: "https://github.com/example/repo//base?version=v1.2.3&timeout=90s", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "unparseable query", + entry: "https://github.com/example/repo?ref=%zz", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "value containing but not beginning with -", + entry: "https://github.com/example/repo?ref=release-1.0&timeout=1m", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "ref beginning with -", + entry: "https://github.com/example/repo?ref=--upload-pack=touch /tmp/pwned", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter ref "--upload-pack=touch /tmp/pwned"`) + }, + }, + { + name: "version beginning with -", + entry: "https://github.com/example/repo?version=-x", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter version "-x"`) + }, + }, + { + name: "percent-encoded ref beginning with -", + entry: "https://github.com/example/repo?ref=%2D%2Dupload-pack%3Dx", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter ref "--upload-pack=x"`) + }, + }, + { + name: "safe ref followed by unsafe duplicate", + entry: "https://github.com/example/repo?ref=main&ref=--upload-pack=x", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter ref "--upload-pack=x"`) + }, + }, + { + name: "safe ref with unsafe version", + entry: "https://github.com/example/repo?ref=main&version=--upload-pack=x", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter version "--upload-pack=x"`) + }, + }, + { + name: "other known parameter beginning with -", + entry: "https://github.com/example/repo?ref=main&timeout=-5", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter timeout "-5"`) + }, + }, + { + name: "unknown parameter beginning with -", + entry: "https://github.com/example/repo?ref=main&future=--upload-pack=x", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter future "--upload-pack=x"`) + }, + }, + { + name: "local path with parameter beginning with -", + entry: "../base?foo=-bar", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter foo "-bar"`) + }, + }, + { + name: "partially unparseable query with parameter beginning with -", + entry: "https://github.com/example/repo?bad=%zz&ref=--upload-pack=x", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, `specifies query parameter ref "--upload-pack=x"`) + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + testCase.assert(t, validateQueryValues(testCase.entry)) + }) + } +} + +func Test_kustomizeValidatorFS_ReadFile(t *testing.T) { + const badRef = "https://github.com/example/repo?ref=--upload-pack=x" + testCases := []struct { + name string + fileName string + content string + assert func(t *testing.T, content []byte, err, recordedErr error) + }{ + { + name: "file does not exist", + fileName: "", + assert: func(t *testing.T, _ []byte, err, recordedErr error) { + require.Error(t, err) + require.NoError(t, recordedErr) + }, + }, + { + name: "non-kustomization file is not inspected", + fileName: "resources.yaml", + content: "resources:\n- " + badRef + "\n", + assert: func(t *testing.T, content []byte, err, recordedErr error) { + require.NoError(t, err) + require.NoError(t, recordedErr) + require.Contains(t, string(content), badRef) + }, + }, + { + name: "unparseable kustomization is passed through", + fileName: "kustomization.yaml", + content: "unknownField: " + badRef + "\n", + assert: func(t *testing.T, content []byte, err, recordedErr error) { + require.NoError(t, err) + require.NoError(t, recordedErr) + require.Contains(t, string(content), badRef) + }, + }, + { + name: "safe kustomization is passed through", + fileName: "kustomization.yaml", + content: `resources: +- ../base +- https://github.com/example/repo?ref=main +`, + assert: func(t *testing.T, content []byte, err, recordedErr error) { + require.NoError(t, err) + require.NoError(t, recordedErr) + require.Contains(t, string(content), "ref=main") + }, + }, + } + for _, field := range []string{ + "resources", + "bases", + "components", + "generators", + "transformers", + "validators", + } { + for _, fileName := range []string{ + "kustomization.yaml", + "kustomization.yml", + "Kustomization", + } { + testCases = append(testCases, struct { + name string + fileName string + content string + assert func(t *testing.T, content []byte, err, recordedErr error) + }{ + name: fmt.Sprintf("unsafe ref in %s of %s", field, fileName), + fileName: fileName, + content: fmt.Sprintf("%s:\n- %s\n", field, badRef), + assert: func(t *testing.T, content []byte, err, recordedErr error) { + require.ErrorContains(t, err, "must not begin with") + require.Nil(t, content) + require.Equal(t, err, recordedErr) + }, + }) + } + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + memFS := filesys.MakeFsInMemory() + path := filepath.Join("/work", "missing") + if testCase.fileName != "" { + path = filepath.Join("/work", testCase.fileName) + require.NoError(t, memFS.WriteFile(path, []byte(testCase.content))) + } + validatingFS := newKustomizeValidatorFS(memFS) + content, err := validatingFS.ReadFile(path) + testCase.assert(t, content, err, validatingFS.Err()) + }) + } +} + +// Test_kustomizeBuild_remoteGitRefInjection is a regression test for +// GHSA-927c-fm24-cq67. Kustomize passes the ref of a remote entry to +// `git fetch` as a literal argument, so a ref such as --upload-pack=<cmd> +// causes git to execute <cmd>. kustomizeBuild must reject such refs -- both +// directly and in remote entries referenced by other remote entries -- while +// still resolving remote entries with safe refs. +func Test_kustomizeBuild_remoteGitRefInjection(t *testing.T) { + gitPath, err := exec.LookPath("git") + if err != nil { + t.Skip("git binary not available") + } + + // A script git would execute as its upload-pack helper if an injected + // option were honored. + markerPath := filepath.Join(t.TempDir(), "pwned") + scriptPath := filepath.Join(t.TempDir(), "upload-pack.sh") + // The script must be executable. + require.NoError(t, os.WriteFile( // nolint: gosec + scriptPath, + []byte("#!/bin/sh\ntouch "+markerPath+"\nexit 1\n"), + 0o700, + )) + maliciousRef := "?ref=--upload-pack=" + scriptPath + + // A repository that generates a single, harmless resource. + safeRepo := newTestGitRepo(t, gitPath, `configMapGenerator: +- name: remote + literals: + - key=value +`) + // A repository whose own kustomization contains a malicious ref. + maliciousRepo := newTestGitRepo(t, gitPath, fmt.Sprintf( + "resources:\n- file://%s%s\n", + safeRepo, maliciousRef, + )) + + testCases := []struct { + name string + resource string + assert func(*testing.T, error) + }{ + { + name: "remote resource with safe ref", + resource: "file://" + safeRepo + "?ref=main", + assert: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "remote resource with malicious ref", + resource: "file://" + safeRepo + maliciousRef, + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, "must not begin with") + }, + }, + { + name: "remote resource that references a malicious ref", + resource: "file://" + maliciousRepo + "?ref=main", + assert: func(t *testing.T, err error) { + require.ErrorContains(t, err, "must not begin with") + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + workDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(workDir, "kustomization.yaml"), + []byte("resources:\n- "+testCase.resource+"\n"), + 0o600, + )) + diskFS, err := securefs.MakeFsOnDiskSecureBuild(workDir) + require.NoError(t, err) + + _, err = kustomizeBuild(diskFS, workDir, nil) + require.NoFileExists(t, markerPath) + testCase.assert(t, err) + }) + } +} + +// newTestGitRepo creates a Git repository with a main branch containing a +// kustomization.yaml with the given content, and returns its path. +func newTestGitRepo(t *testing.T, gitPath, kustomization string) string { + t.Helper() + dir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(dir, "kustomization.yaml"), + []byte(kustomization), + 0o600, + )) + for _, args := range [][]string{ + {"init", "--initial-branch=main"}, + {"add", "."}, + { + "-c", "user.name=test", + "-c", "[email protected]", + "-c", "commit.gpgsign=false", + "commit", "-m", "initial commit", + }, + } { + cmd := exec.Command(gitPath, args...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + require.NoError(t, err, string(out)) + } + return dir +} ++++++ kargo-cli.obsinfo ++++++ --- /var/tmp/diff_new_pack.LdkaMY/_old 2026-10-09 21:44:39.533488812 +0200 +++ /var/tmp/diff_new_pack.LdkaMY/_new 2026-10-09 21:44:39.538489023 +0200 @@ -1,5 +1,5 @@ name: kargo-cli -version: 1.12.2 -mtime: 1791311042 -commit: 2b5f3a2f12c1eb6156253ba71d44818d4262c865 +version: 1.12.3 +mtime: 1791472368 +commit: d544d7c6a9d4d99d096df52dcb8357375c26b729 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/kargo-cli/vendor.tar.gz /work/SRC/openSUSE:Factory/.kargo-cli.new.3698736/vendor.tar.gz differ: char 133, line 2
