CVE-2025-65965
Description
Grype is a vulnerability scanner for container images and filesystems. A credential disclosure vulnerability was found in Grype, affecting versions 0.68.0 through 0.104.0. If registry credentials are defined and the output of grype is written using the --file or --output json=<file> option, the registry credentials will be included unsanitized in the output file. This issue has been patched in version 0.104.1. Users running affected versions of grype can work around this vulnerability by redirecting stdout to a file instead of using the --file or --output options.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/anchore/grypeGo | >= 0.68.0, < 0.104.1 | 0.104.1 |
Affected products
1Patches
239f7fa17af27fix: redact during file output (#3068)
4 files changed · +109 −5
cmd/grype/cli/commands/root.go+3 −0 modified@@ -236,6 +236,9 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs log.WithFields("time", time.Since(startTime)).Info("found vulnerability matches") startTime = time.Now() + // clear out the registry auth information to avoid including possibly sensitive information in the report + opts.Registry.Auth = nil + model, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, dbInfo(status, vp), models.SortStrategy(opts.SortBy.Criteria), opts.Timestamp) if err != nil { return fmt.Errorf("failed to create document: %w", err)
cmd/grype/cli/options/registry.go+4 −4 modified@@ -21,7 +21,7 @@ type RegistryCredentials struct { type registry struct { InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` - Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` + Auth []RegistryCredentials `yaml:"auth" json:"auth,omitempty" mapstructure:"auth"` CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` } @@ -82,9 +82,9 @@ func (cfg *registry) ToOptions() *image.RegistryOptions { for i, a := range cfg.Auth { auth[i] = image.RegistryCredentials{ Authority: a.Authority, - Username: a.Username.String(), - Password: a.Password.String(), - Token: a.Token.String(), + Username: string(a.Username), + Password: string(a.Password), + Token: string(a.Token), ClientCert: a.TLSCert, ClientKey: a.TLSKey, }
cmd/grype/cli/options/secret.go+9 −1 modified@@ -21,5 +21,13 @@ func (r *secret) PostLoad() error { } func (r secret) String() string { - return string(r) + if r == "" { + return "" + } + // match the redactor's behavior, replacing with 7 asterisks + return "*******" +} + +func (r secret) MarshalText() ([]byte, error) { + return []byte(r.String()), nil }
test/cli/registry_auth_test.go+93 −0 modified@@ -1,8 +1,12 @@ package cli import ( + "os" + "path/filepath" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestRegistryAuth(t *testing.T) { @@ -97,3 +101,92 @@ func TestRegistryAuth(t *testing.T) { }) } } + +func TestRegistryAuthRedactions(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "output.json") + + assertNotInFile := func(text string) traitAssertion { + return func(tb testing.TB, stdout, stderr string, rc int) { + contents, err := os.ReadFile(tmp) + require.NoError(tb, err) + require.NotEmpty(tb, contents) + require.NotContains(tb, string(contents), text) + } + } + + tests := []struct { + name string + args []string + env map[string]string + assertions []traitAssertion + }{ + { + name: "use creds", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json"}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", + "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInOutput("foobar-username"), + assertNotInOutput("foobar-password"), + }, + }, + { + name: "use token", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json"}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInOutput("foobar-token"), + }, + }, + { + name: "use creds file", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json", "--file", tmp}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", + "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInFile("foobar-username"), + assertNotInFile("foobar-password"), + assertNotInOutput("foobar-username"), + assertNotInOutput("foobar-password"), + }, + }, + { + name: "use token file", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json", "--file", tmp}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInFile("foobar-token"), + assertNotInOutput("foobar-token"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _ = os.Remove(tmp) // ok to fail + cmd, stdout, stderr := runGrype(t, test.env, test.args...) + for _, traitAssertionFn := range test.assertions { + traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + if t.Failed() { + fileContents, _ := os.ReadFile(tmp) + t.Log("FILE:\n", string(fileContents)) + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + } + }) + } +}
c99f79de49a5fix: redact during file output
4 files changed · +109 −5
cmd/grype/cli/commands/root.go+3 −0 modified@@ -236,6 +236,9 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs log.WithFields("time", time.Since(startTime)).Info("found vulnerability matches") startTime = time.Now() + // clear out the registry auth information to avoid including possibly sensitive information in the report + opts.Registry.Auth = nil + model, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, dbInfo(status, vp), models.SortStrategy(opts.SortBy.Criteria), opts.Timestamp) if err != nil { return fmt.Errorf("failed to create document: %w", err)
cmd/grype/cli/options/registry.go+4 −4 modified@@ -21,7 +21,7 @@ type RegistryCredentials struct { type registry struct { InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` - Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` + Auth []RegistryCredentials `yaml:"auth" json:"auth,omitempty" mapstructure:"auth"` CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` } @@ -82,9 +82,9 @@ func (cfg *registry) ToOptions() *image.RegistryOptions { for i, a := range cfg.Auth { auth[i] = image.RegistryCredentials{ Authority: a.Authority, - Username: a.Username.String(), - Password: a.Password.String(), - Token: a.Token.String(), + Username: string(a.Username), + Password: string(a.Password), + Token: string(a.Token), ClientCert: a.TLSCert, ClientKey: a.TLSKey, }
cmd/grype/cli/options/secret.go+9 −1 modified@@ -21,5 +21,13 @@ func (r *secret) PostLoad() error { } func (r secret) String() string { - return string(r) + if r == "" { + return "" + } + // match the redactor's behavior, replacing with 7 asterisks + return "*******" +} + +func (r secret) MarshalText() ([]byte, error) { + return []byte(r.String()), nil }
test/cli/registry_auth_test.go+93 −0 modified@@ -1,8 +1,12 @@ package cli import ( + "os" + "path/filepath" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestRegistryAuth(t *testing.T) { @@ -97,3 +101,92 @@ func TestRegistryAuth(t *testing.T) { }) } } + +func TestRegistryAuthRedactions(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "output.json") + + assertNotInFile := func(text string) traitAssertion { + return func(tb testing.TB, stdout, stderr string, rc int) { + contents, err := os.ReadFile(tmp) + require.NoError(tb, err) + require.NotEmpty(tb, contents) + require.NotContains(tb, string(contents), text) + } + } + + tests := []struct { + name string + args []string + env map[string]string + assertions []traitAssertion + }{ + { + name: "use creds", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json"}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", + "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInOutput("foobar-username"), + assertNotInOutput("foobar-password"), + }, + }, + { + name: "use token", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json"}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInOutput("foobar-token"), + }, + }, + { + name: "use creds file", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json", "--file", tmp}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", + "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInFile("foobar-username"), + assertNotInFile("foobar-password"), + assertNotInOutput("foobar-username"), + assertNotInOutput("foobar-password"), + }, + }, + { + name: "use token file", + args: []string{"-vv", "sbom:test-fixtures/sbom-grype-source.json", "-o", "json", "--file", tmp}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", + }, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertNotInFile("foobar-token"), + assertNotInOutput("foobar-token"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _ = os.Remove(tmp) // ok to fail + cmd, stdout, stderr := runGrype(t, test.env, test.args...) + for _, traitAssertionFn := range test.assertions { + traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + if t.Failed() { + fileContents, _ := os.ReadFile(tmp) + t.Log("FILE:\n", string(fileContents)) + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + } + }) + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-6gxw-85q2-q646ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-65965ghsaADVISORY
- github.com/anchore/grype/commit/39f7fa17af2739cafe9b27176d4a68f7c05f21c1nvdWEB
- github.com/anchore/grype/commit/c99f79de49a58dc16d7fd8f35160b169b87db9deghsaWEB
- github.com/anchore/grype/pull/3068nvdWEB
- github.com/anchore/grype/security/advisories/GHSA-6gxw-85q2-q646nvdWEB
News mentions
0No linked articles in our index yet.