CVE-2022-23538: Insufficiently Protected Credentials (CWE-522)
value: github.com/sylabs/scs-library-client is the Go client for the Singularity Container Services (SCS) Container Library Service. When the scs-library-client is used to pull a container image, with authentication, the HTTP Authorization header sent by the client to the library service may be incorrectly leaked to an S3 backing storage provider. This occurs in a specific flow, where the library service redirects the client to a backing S3 storage server, to perform a multi-part concurrent download. Depending on site configuration, the S3 service may be provided by a third party. An attacker with access to the S3 service may be able to extract user credentials, allowing them to impersonate the user. The vulnerable multi-part concurrent download flow, with redirect to S3, is only used when communicating with a Singularity Enterprise 1.x installation, or third party server implementing this flow. Interaction with Singularity Enterprise 2.x, and Singularity Container Services (cloud.sylabs.io), does not trigger the vulnerable flow. We encourage all users to update. Users who interact with a Singularity Enterprise 1.x installation, using a 3rd party S3 storage service, are advised to revoke and recreate their authentication tokens within Singularity Enterprise. There is no workaround available at this time.
CWE description: The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
CVSS v3 base score: 7.6
Published: 2023-01-17T21:15Z
Repository: https://github.com/sylabs/scs-library-client
Language: Go
Files changed: client/pull.go
Commit date: 2023-01-06 09:47:09 -0500
Commit message: fix: omit credentials for redirected URLs

Diff: diff --git a/client/pull.go b/client/pull.go
index 067eb73..8cbbb34 100644
--- a/client/pull.go
+++ b/client/pull.go
@@ -234,17 +234,32 @@ func (c *Client) legacyDownloadImage(ctx context.Context, arch, name, tag string
 	img, err := c.GetImage(ctx, arch, fmt.Sprintf("%v:%v", name, tag))
 	if err != nil {
 		return err
 	}
 
+	redirectURL, err := url.Parse(res.Header.Get("Location"))
+	if err != nil {
+		return err
+	}
+
 	var creds credentials
-	if c.AuthToken != "" {
+	if c.AuthToken != "" && samehost(c.BaseURL, redirectURL) {
+		// Only include credentials if redirected to same host as base URL
 		creds = bearerTokenCredentials{authToken: c.AuthToken}
 	}
 
-	// Use uri from Location header to download artifact
-	return c.multipartDownload(ctx, res.Header.Get("Location"), creds, dst, img.Size, spec, pb)
+	// Use redirect URL to download artifact
+	return c.multipartDownload(ctx, redirectURL.String(), creds, dst, img.Size, spec, pb)
+}
+
+// samehost returns true if host1 and host2 are, in fact, the same host by
+// comparing scheme (https == https) and host, including port.
+//
+// Hosts will be treated as dissimilar if one host includes domain suffix
+// and the other does not, even if the host names match.
+func samehost(host1, host2 *url.URL) bool {
+	return strings.EqualFold(host1.Scheme, host2.Scheme) && strings.EqualFold(host1.Host, host2.Host)
 }
 
 func parseContentLengthHeader(val string) (int64, error) {
 	if val == "" {
 		return int64(-1), nil

Vulnerable code: 	if c.AuthToken != "" {
	// Use uri from Location header to download artifact
	return c.multipartDownload(ctx, res.Header.Get("Location"), creds, dst, img.Size, spec, pb)
Fixed code: 	redirectURL, err := url.Parse(res.Header.Get("Location"))
	if err != nil {
		return err
	}

	if c.AuthToken != "" && samehost(c.BaseURL, redirectURL) {
		// Only include credentials if redirected to same host as base URL
	// Use redirect URL to download artifact
	return c.multipartDownload(ctx, redirectURL.String(), creds, dst, img.Size, spec, pb)
}

// samehost returns true if host1 and host2 are, in fact, the same host by
// comparing scheme (https == https) and host, including port.
//
// Hosts will be treated as dissimilar if one host includes domain suffix
// and the other does not, even if the host names match.
func samehost(host1, host2 *url.URL) bool {
	return strings.EqualFold(host1.Scheme, host2.Scheme) && strings.EqualFold(host1.Host, host2.Host)