CVE-2024-31216
Description
The source-controller is a Kubernetes operator, specialised in artifacts acquisition from external sources such as Git, OCI, Helm repositories and S3-compatible buckets. The source-controller implements the source.toolkit.fluxcd.io API and is a core component of the GitOps toolkit. Prior to version 1.2.5, when source-controller was configured to use an Azure SAS token when connecting to Azure Blob Storage, the token was logged along with the Azure URL when the controller encountered a connection error. An attacker with access to the source-controller logs could use the token to gain access to the Azure Blob Storage until the token expires. This vulnerability was fixed in source-controller v1.2.5. There is no workaround for this vulnerability except for using a different auth mechanism such as Azure Workload Identity.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/fluxcd/source-controllerGo | < 1.2.5 | 1.2.5 |
Patches
1915d1a072a4fMerge pull request #1430 from fluxcd/sanitze-bucker-errors
3 files changed · +219 −2
internal/controller/bucket_controller.go+2 −2 modified@@ -728,7 +728,7 @@ func fetchEtagIndex(ctx context.Context, provider BucketProvider, obj *bucketv1. path := filepath.Join(tempDir, sourceignore.IgnoreFile) if _, err := provider.FGetObject(ctxTimeout, obj.Spec.BucketName, sourceignore.IgnoreFile, path); err != nil { if !provider.ObjectIsNotFound(err) { - return err + return fmt.Errorf("failed to get Etag for '%s' object: %w", sourceignore.IgnoreFile, serror.SanitizeError(err)) } } ps, err := sourceignore.ReadIgnoreFile(path, nil) @@ -792,7 +792,7 @@ func fetchIndexFiles(ctx context.Context, provider BucketProvider, obj *bucketv1 index.Delete(k) return nil } - return fmt.Errorf("failed to get '%s' object: %w", k, err) + return fmt.Errorf("failed to get '%s' object: %w", k, serror.SanitizeError(err)) } if t != etag { index.Add(k, etag)
internal/error/sanitized.go+76 −0 added@@ -0,0 +1,76 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "fmt" + "net/url" + "regexp" +) + +type SanitizedError struct { + err string +} + +func (e SanitizedError) Error() string { + return e.err +} + +// SanitizeError extracts all URLs from the error message +// and replaces them with the URL without the query string. +func SanitizeError(err error) SanitizedError { + errorMessage := err.Error() + for _, u := range extractURLs(errorMessage) { + urlWithoutQueryString, err := removeQueryString(u) + if err == nil { + re, err := regexp.Compile(fmt.Sprintf("%s*", regexp.QuoteMeta(u))) + if err == nil { + errorMessage = re.ReplaceAllString(errorMessage, urlWithoutQueryString) + } + } + } + + return SanitizedError{errorMessage} +} + +// removeQueryString takes a URL string as input and returns the URL without the query string. +func removeQueryString(urlStr string) (string, error) { + // Parse the URL. + u, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + // Rebuild the URL without the query string. + u.RawQuery = "" + return u.String(), nil +} + +// extractURLs takes a log message as input and returns the URLs found. +func extractURLs(logMessage string) []string { + // Define a regular expression to match a URL. + // This is a simple pattern and might need to be adjusted depending on the log message format. + urlRegex := regexp.MustCompile(`https?://[^\s]+`) + + // Find the first match in the log message. + matches := urlRegex.FindAllString(logMessage, -1) + if len(matches) == 0 { + return []string{} + } + + return matches +}
internal/error/sanitized_test.go+141 −0 added@@ -0,0 +1,141 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "errors" + "testing" + + . "github.com/onsi/gomega" +) + +func Test_extractURLs(t *testing.T) { + + tests := []struct { + name string + logMessage string + wantUrls []string + }{ + { + name: "Log Contains single URL", + logMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml?se=2024-05-01T16%3A28%3A26Z&sig=Signature&sp=rl&sr=c&st=2024-02-01T16%3A28%3A26Z&sv=2022-11-02\": dial tcp 20.60.53.129:443: connect: connection refused", + wantUrls: []string{"https://blobstorage.blob.core.windows.net/container/index.yaml?se=2024-05-01T16%3A28%3A26Z&sig=Signature&sp=rl&sr=c&st=2024-02-01T16%3A28%3A26Z&sv=2022-11-02\":"}, + }, + { + name: "Log Contains multiple URL", + logMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml?abc=es https://blobstorage1.blob.core.windows.net/container/index.yaml?abc=no : dial tcp 20.60.53.129:443: connect: connection refused", + wantUrls: []string{ + "https://blobstorage.blob.core.windows.net/container/index.yaml?abc=es", + "https://blobstorage1.blob.core.windows.net/container/index.yaml?abc=no", + }, + }, + { + name: "Log Contains No URL", + logMessage: "Log message without URL", + wantUrls: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + urls := extractURLs(tt.logMessage) + + g.Expect(len(urls)).To(Equal(len(tt.wantUrls))) + for i := range tt.wantUrls { + g.Expect(urls[i]).To(Equal(tt.wantUrls[i])) + } + }) + } +} + +func Test_removeQueryString(t *testing.T) { + + tests := []struct { + name string + urlStr string + wantUrl string + }{ + { + name: "URL with query string", + urlStr: "https://blobstorage.blob.core.windows.net/container/index.yaml?se=2024-05-01T16%3A28%3A26Z&sig=Signature&sp=rl&sr=c&st=2024-02-01T16%3A28%3A26Z&sv=2022-11-02", + wantUrl: "https://blobstorage.blob.core.windows.net/container/index.yaml", + }, + { + name: "URL without query string", + urlStr: "https://blobstorage.blob.core.windows.net/container/index.yaml", + wantUrl: "https://blobstorage.blob.core.windows.net/container/index.yaml", + }, + { + name: "URL with query string and port", + urlStr: "https://blobstorage.blob.core.windows.net:443/container/index.yaml?se=2024-05-01T16%3A28%3A26Z&sig=Signature&sp=rl&sr=c&st=2024-02-01T16%3A28%3A26Z&sv=2022-11-02", + wantUrl: "https://blobstorage.blob.core.windows.net:443/container/index.yaml", + }, + { + name: "Invalid URL", + urlStr: "NoUrl", + wantUrl: "NoUrl", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + urlWithoutQueryString, err := removeQueryString(tt.urlStr) + + g.Expect(err).To(BeNil()) + g.Expect(urlWithoutQueryString).To(Equal(tt.wantUrl)) + }) + } +} + +func Test_SanitizeError(t *testing.T) { + + tests := []struct { + name string + errMessage string + wantErrMessage string + }{ + { + name: "Log message with URL with query string", + errMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml?se=2024-05-01T16%3A28%3A26Z&sig=Signature&sp=rl&sr=c&st=2024-02-01T16%3A28%3A26Z&sv=2022-11-02\": dial tcp 20.60.53.129:443: connect: connection refused", + wantErrMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml dial tcp 20.60.53.129:443: connect: connection refused", + }, + { + name: "Log message without URL", + errMessage: "Log message contains no URL", + wantErrMessage: "Log message contains no URL", + }, + + { + name: "Log message with multiple Urls", + errMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml?abc=es https://blobstorage1.blob.core.windows.net/container/index.yaml?abc=no dial tcp 20.60.53.129:443: connect: connection refused", + wantErrMessage: "Get \"https://blobstorage.blob.core.windows.net/container/index.yaml https://blobstorage1.blob.core.windows.net/container/index.yaml dial tcp 20.60.53.129:443: connect: connection refused", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := SanitizeError(errors.New(tt.errMessage)) + g.Expect(err.Error()).To(Equal(tt.wantErrMessage)) + }) + } +}
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
5- github.com/advisories/GHSA-v554-xwgw-hc3wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-31216ghsaADVISORY
- github.com/fluxcd/source-controller/commit/915d1a072a4f37dd460ba33079dc094aa6e72fa9nvdWEB
- github.com/fluxcd/source-controller/pull/1430nvdWEB
- github.com/fluxcd/source-controller/security/advisories/GHSA-v554-xwgw-hc3wnvdWEB
News mentions
0No linked articles in our index yet.