CVE-2021-21287: Server-Side Request Forgery (SSRF) (CWE-918)
value: MinIO is a High Performance Object Storage released under Apache License v2.0. In MinIO before version RELEASE.2021-01-30T00-20-58Z there is a server-side request forgery vulnerability. The target application may have functionality for importing data from a URL, publishing data to a URL, or otherwise reading data from a URL that can be tampered with. The attacker modifies the calls to this functionality by supplying a completely different URL or by manipulating how URLs are built (path traversal etc.). In a Server-Side Request Forgery (SSRF) attack, the attacker can abuse functionality on the server to read or update internal resources. The attacker can supply or modify a URL which the code running on the server will read or submit data, and by carefully selecting the URLs, the attacker may be able to read server configuration such as AWS metadata, connect to internal services like HTTP enabled databases, or perform post requests towards internal services which are not intended to be exposed. This is fixed in version RELEASE.2021-01-30T00-20-58Z, all users are advised to upgrade. As a workaround you can disable the browser front-end with "MINIO_BROWSER=off" environment variable.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.7
Published: 2021-02-01T18:15Z
Repository: https://github.com/minio/minio
Language: Go
Files changed: cmd/api-errors.go, cmd/web-handlers.go
Commit date: 2021-01-25 10:15:03 -0800
Commit message: fix: LoginSTS should be an inline implementation (#11337)

STS tokens can be obtained by using local APIs
once the remote JWT token is presented, current
code was not validating the incoming token in the
first place and was incorrectly making a network
operation using that token.

For the most part this always works without issues,
but under adversarial scenarios it exposes client
to hand-craft a request that can reach internal
services without authentication.

This kind of proxying should be avoided before
validating the incoming token.
Diff: diff --git a/cmd/api-errors.go b/cmd/api-errors.go
index fdbe40af7..3ebbf42e9 100644
--- a/cmd/api-errors.go
+++ b/cmd/api-errors.go
@@ -2123,10 +2123,16 @@ func toAPIError(ctx context.Context, err error) APIError {
 				Code:           string(e.ServiceCode()),
 				Description:    e.Error(),
 				HTTPStatusCode: e.Response().StatusCode,
 			}
 			// Add more Gateway SDKs here if any in future.
+		default:
+			apiErr = APIError{
+				Code:           apiErr.Code,
+				Description:    fmt.Sprintf("%s: cause(%v)", apiErr.Description, err),
+				HTTPStatusCode: apiErr.HTTPStatusCode,
+			}
 		}
 	}
 
 	return apiErr
 }
diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go
index 1fceaad03..63d820130 100644
--- a/cmd/web-handlers.go
+++ b/cmd/web-handlers.go
@@ -18,11 +18,10 @@ package cmd
 
 import (
 	"context"
 	"crypto/subtle"
 	"encoding/json"
-	"encoding/xml"
 	"errors"
 	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -2200,58 +2199,65 @@ func (web *webAPIHandlers) GetDiscoveryDoc(r *http.Request, args *WebGenericArgs
 // LoginSTSArgs - login arguments.
 type LoginSTSArgs struct {
 	Token string `json:"token" form:"token"`
 }
 
+var errSTSNotInitialized = errors.New("STS API not initialized, please configure STS support")
+
 // LoginSTS - STS user login handler.
 func (web *webAPIHandlers) LoginSTS(r *http.Request, args *LoginSTSArgs, reply *LoginRep) error {
 	ctx := newWebContext(r, args, "WebLoginSTS")
 
-	v := url.Values{}
-	v.Set("Action", webIdentity)
-	v.Set("WebIdentityToken", args.Token)
-	v.Set("Version", stsAPIVersion)
-
-	scheme := "http"
-	if sourceScheme := handlers.GetSourceScheme(r); sourceScheme != "" {
-		scheme = sourceScheme
-	}
-	if globalIsTLS {
-		scheme = "https"
+	if globalOpenIDValidators == nil {
+		return toJSONError(ctx, errSTSNotInitialized)
 	}
 
-	u := &url.URL{
-		Scheme: scheme,
-		Host:   r.Host,
+	v, err := globalOpenIDValidators.Get("jwt")
+	if err != nil {
+		logger.LogIf(ctx, err)
+		return toJSONError(ctx, errSTSNotInitialized)
 	}
 
-	u.RawQuery = v.Encode()
-
-	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
+	m, err := v.Validate(args.Token, "")
 	if err != nil {
 		return toJSONError(ctx, err)
 	}
 
-	clnt := &http.Client{
-		Transport: NewGatewayHTTPTransport(),
+	// JWT has requested a custom claim with policy value set.
+	// This is a MinIO STS API specific value, this value should
+	// be set and configured on your identity provider as part of
+	// JWT custom claims.
+	var policyName string
+	policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
+	if ok {
+		policyName = globalIAMSys.CurrentPolicies(strings.Join(policySet.ToSlice(), ","))
 	}
-	resp, err := clnt.Do(req)
+	if policyName == "" && globalPolicyOPA == nil {
+		return toJSONError(ctx, fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
+	}
+	m[iamPolicyClaimNameOpenID()] = policyName
+
+	secret := globalActiveCred.SecretKey
+	cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
 	if err != nil {
 		return toJSONError(ctx, err)
 	}
-	defer xhttp.DrainBody(resp.Body)
 
-	if resp.StatusCode != http.StatusOK {
-		return toJSONError(ctx, errors.New(resp.Status))
+	// Set the newly generated credentials.
+	if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
+		return toJSONError(ctx, err)
 	}
 
-	a := AssumeRoleWithWebIdentityResponse{}
-	if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil {
-		return toJSONError(ctx, err)
+	// Notify all other MinIO peers to reload temp users
+	for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) {
+		if nerr.Err != nil {
+			logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+			logger.LogIf(ctx, nerr.Err)
+		}
 	}
 
-	reply.Token = a.Result.Credentials.SessionToken
+	reply.Token = cred.SessionToken
 	reply.UIVersion = browser.UIVersion
 	return nil
 }
 
 // toJSONError converts regular errors into more user friendly
@@ -2302,10 +2308,12 @@ func toWebAPIError(ctx context.Context, err error) APIError {
 		return APIError{
 			Code:           "WebTokenMissing",
 			HTTPStatusCode: http.StatusBadRequest,
 			Description:    err.Error(),
 		}
+	case errSTSNotInitialized:
+		return APIError(stsErrCodes.ToSTSErr(ErrSTSNotInitialized))
 	case errServerNotInitialized:
 		return APIError{
 			Code:           "XMinioServerNotInitialized",
 			HTTPStatusCode: http.StatusServiceUnavailable,
 			Description:    err.Error(),

Vulnerable code: 	"encoding/xml"
	v := url.Values{}
	v.Set("Action", webIdentity)
	v.Set("WebIdentityToken", args.Token)
	v.Set("Version", stsAPIVersion)

	scheme := "http"
	if sourceScheme := handlers.GetSourceScheme(r); sourceScheme != "" {
		scheme = sourceScheme
	}
	if globalIsTLS {
		scheme = "https"
	u := &url.URL{
		Scheme: scheme,
		Host:   r.Host,
	u.RawQuery = v.Encode()

	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
	clnt := &http.Client{
		Transport: NewGatewayHTTPTransport(),
	resp, err := clnt.Do(req)
	defer xhttp.DrainBody(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return toJSONError(ctx, errors.New(resp.Status))
	a := AssumeRoleWithWebIdentityResponse{}
	if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil {
		return toJSONError(ctx, err)
	reply.Token = a.Result.Credentials.SessionToken
Fixed code: 		default:
			apiErr = APIError{
				Code:           apiErr.Code,
				Description:    fmt.Sprintf("%s: cause(%v)", apiErr.Description, err),
				HTTPStatusCode: apiErr.HTTPStatusCode,
			}
var errSTSNotInitialized = errors.New("STS API not initialized, please configure STS support")

	if globalOpenIDValidators == nil {
		return toJSONError(ctx, errSTSNotInitialized)
	v, err := globalOpenIDValidators.Get("jwt")
	if err != nil {
		logger.LogIf(ctx, err)
		return toJSONError(ctx, errSTSNotInitialized)
	m, err := v.Validate(args.Token, "")
	// JWT has requested a custom claim with policy value set.
	// This is a MinIO STS API specific value, this value should
	// be set and configured on your identity provider as part of
	// JWT custom claims.
	var policyName string
	policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
	if ok {
		policyName = globalIAMSys.CurrentPolicies(strings.Join(policySet.ToSlice(), ","))
	if policyName == "" && globalPolicyOPA == nil {
		return toJSONError(ctx, fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
	}
	m[iamPolicyClaimNameOpenID()] = policyName

	secret := globalActiveCred.SecretKey
	cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
	// Set the newly generated credentials.
	if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
		return toJSONError(ctx, err)
	// Notify all other MinIO peers to reload temp users
	for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) {
		if nerr.Err != nil {
			logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
			logger.LogIf(ctx, nerr.Err)
		}
	reply.Token = cred.SessionToken
	case errSTSNotInitialized:
		return APIError(stsErrCodes.ToSTSErr(ErrSTSNotInitialized))