CVE-2021-21362: Incorrect Authorization (CWE-863)
value: MinIO is an open-source high performance object storage service and it is API compatible with Amazon S3 cloud storage service. In MinIO before version RELEASE.2021-03-04T00-53-13Z it is possible to bypass a readOnly policy by creating a temporary 'mc share upload' URL. Everyone is impacted who uses MinIO multi-users. This is fixed in version RELEASE.2021-03-04T00-53-13Z. As a workaround, one can disable uploads with `Content-Type: multipart/form-data` as mentioned in the S3 API RESTObjectPOST docs by using a proxy in front of MinIO.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 6.5
Published: 2021-03-08T19:15Z
Repository: https://github.com/minio/minio
Language: Go
Files changed: cmd/auth-handler.go, cmd/bucket-handlers.go, cmd/signature-v2.go, cmd/signature-v2_test.go, cmd/signature-v4.go, cmd/signature-v4_test.go
Commit date: 2021-03-03 08:47:08 -0800
Commit message: fix: missing user policy enforcement in PostPolicyHandler (#11682)


Diff: diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go
index 260699a84..1f71c1dc9 100644
--- a/cmd/auth-handler.go
+++ b/cmd/auth-handler.go
@@ -183,16 +183,16 @@ func getSessionToken(r *http.Request) (token string) {
 }
 
 // Fetch claims in the security token returned by the client, doesn't return
 // errors - upon errors the returned claims map will be empty.
 func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
-	claims, _ := getClaimsFromToken(r, getSessionToken(r))
+	claims, _ := getClaimsFromToken(getSessionToken(r))
 	return claims
 }
 
 // Fetch claims in the security token returned by the client.
-func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, error) {
+func getClaimsFromToken(token string) (map[string]interface{}, error) {
 	claims := xjwt.NewMapClaims()
 	if token == "" {
 		return claims.Map(), nil
 	}
 
@@ -235,11 +235,11 @@ func getClaimsFromToken(r *http.Request, token string) (map[string]interface{},
 		// base64 encoded, decode it. Decoding fails reject such requests.
 		spBytes, err := base64.StdEncoding.DecodeString(sp)
 		if err != nil {
 			// Base64 decoding fails, we should log to indicate
 			// something is malforming the request sent by client.
-			logger.LogIf(r.Context(), err, logger.Application)
+			logger.LogIf(GlobalContext, err, logger.Application)
 			return nil, errAuthentication
 		}
 		claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
 	}
 
@@ -256,11 +256,11 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
 		token = cred.SessionToken
 	}
 	if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
 		return nil, ErrInvalidToken
 	}
-	claims, err := getClaimsFromToken(r, token)
+	claims, err := getClaimsFromToken(token)
 	if err != nil {
 		return nil, toAPIErrorCode(r.Context(), err)
 	}
 	return claims, ErrNone
 }
diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go
index 66ef57f40..49f57f2f5 100644
--- a/cmd/bucket-handlers.go
+++ b/cmd/bucket-handlers.go
@@ -15,19 +15,19 @@
  */
 
 package cmd
 
 import (
+	"crypto/subtle"
 	"encoding/base64"
 	"encoding/xml"
 	"fmt"
 	"io"
 	"net/http"
 	"net/textproto"
 	"net/url"
 	"path"
-	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
 	"sync"
 
@@ -335,11 +335,11 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
 		// Set delimiter value for "s3:delimiter" policy conditionals.
 		r.Header.Set("delimiter", SlashSeparator)
 
 		// err will be nil here as we already called this function
 		// earlier in this request.
-		claims, _ := getClaimsFromToken(r, getSessionToken(r))
+		claims, _ := getClaimsFromToken(getSessionToken(r))
 		n := 0
 		// Use the following trick to filter in place
 		// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
 		for _, bucketInfo := range bucketsInfo {
 			if globalIAMSys.IsAllowed(iampolicy.Args{
@@ -795,17 +795,19 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
 	size := r.ContentLength
 	if size < 0 {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
 		return
 	}
+
 	resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
 	if err != nil {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
 		return
 	}
-	// Make sure that the URL  does not contain object name.
-	if bucket != filepath.Clean(resource[1:]) {
+
+	// Make sure that the URL does not contain object name.
+	if bucket != path.Clean(resource[1:]) {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
 		return
 	}
 
 	// Here the parameter is the size of the form data that should
@@ -844,11 +846,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
 
 	// Close multipart file
 	defer fileBody.Close()
 
 	formValues.Set("Bucket", bucket)
-
 	if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
 		// S3 feature to replace ${filename} found in Key form field
 		// by the filename attribute passed in multipart
 		formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
 	}
@@ -864,16 +865,55 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
 			return
 		}
 	}
 
 	// Verify policy signature.
-	errCode := doesPolicySignatureMatch(formValues)
+	cred, errCode := doesPolicySignatureMatch(formValues)
 	if errCode != ErrNone {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r))
 		return
 	}
 
+	// Once signature is validated, check if the user has
+	// explicit permissions for the user.
+	{
+		token := formValues.Get(xhttp.AmzSecurityToken)
+		if token != "" && cred.AccessKey == "" {
+			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL, guessIsBrowserReq(r))
+			return
+		}
+
+		if cred.IsServiceAccount() && token == "" {
+			token = cred.SessionToken
+		}
+
+		if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
+			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL, guessIsBrowserReq(r))
+			return
+		}
+
+		// Extract claims if any.
+		claims, err := getClaimsFromToken(token)
+		if err != nil {
+			writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
+			return
+		}
+
+		if !globalIAMSys.IsAllowed(iampolicy.Args{
+			AccountName:     cred.AccessKey,
+			Action:          iampolicy.PutObjectAction,
+			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
+			BucketName:      bucket,
+			ObjectName:      object,
+			IsOwner:         globalActiveCred.AccessKey == cred.AccessKey,
+			Claims:          claims,
+		}) {
+			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL, guessIsBrowserReq(r))
+			return
+		}
+	}
+
 	policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
 	if err != nil {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL, guessIsBrowserReq(r))
 		return
 	}
diff --git a/cmd/signature-v2.go b/cmd/signature-v2.go
index 97ca4099e..f0fa6cbd9 100644
--- a/cmd/signature-v2.go
+++ b/cmd/signature-v2.go
@@ -73,24 +73,22 @@ const (
 	signV2Algorithm = "AWS"
 )
 
 // AWS S3 Signature V2 calculation rule is give here:
 // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign
-
-func doesPolicySignatureV2Match(formValues http.Header) APIErrorCode {
-	cred := globalActiveCred
+func doesPolicySignatureV2Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
 	accessKey := formValues.Get(xhttp.AmzAccessKeyID)
 	cred, _, s3Err := checkKeyValid(accessKey)
 	if s3Err != ErrNone {
-		return s3Err
+		return cred, s3Err
 	}
 	policy := formValues.Get("Policy")
 	signature := formValues.Get(xhttp.AmzSignatureV2)
 	if !compareSignatureV2(signature, calculateSignatureV2(policy, cred.SecretKey)) {
-		return ErrSignatureDoesNotMatch
+		return cred, ErrSignatureDoesNotMatch
 	}
-	return ErrNone
+	return cred, ErrNone
 }
 
 // Escape encodedQuery string into unescaped list of query params, returns error
 // if any while unescaping the values.
 func unescapeQueries(encodedQuery string) (unescapedQueries []string, err error) {
diff --git a/cmd/signature-v2_test.go b/cmd/signature-v2_test.go
index 13930709a..bf52c9b71 100644
--- a/cmd/signature-v2_test.go
+++ b/cmd/signature-v2_test.go
@@ -263,11 +263,11 @@ func TestDoesPolicySignatureV2Match(t *testing.T) {
 	for i, test := range testCases {
 		formValues := make(http.Header)
 		formValues.Set("Awsaccesskeyid", test.accessKey)
 		formValues.Set("Signature", test.signature)
 		formValues.Set("Policy", test.policy)
-		errCode := doesPolicySignatureV2Match(formValues)
+		_, errCode := doesPolicySignatureV2Match(formValues)
 		if errCode != test.errCode {
 			t.Fatalf("(%d) expected to get %s, instead got %s", i+1, niceError(test.errCode), niceError(errCode))
 		}
 	}
 }
diff --git a/cmd/signature-v4.go b/cmd/signature-v4.go
index 757a22092..63ee1cc25 100644
--- a/cmd/signature-v4.go
+++ b/cmd/signature-v4.go
@@ -37,10 +37,11 @@ import (
 	"time"
 
 	"github.com/minio/minio-go/v7/pkg/s3utils"
 	"github.com/minio/minio-go/v7/pkg/set"
 	xhttp "github.com/minio/minio/cmd/http"
+	"github.com/minio/minio/pkg/auth"
 )
 
 // AWS Signature Version '4' constants.
 const (
 	signV4Algorithm = "AWS4-HMAC-SHA256"
@@ -147,11 +148,11 @@ func getSigningKey(secretKey string, t time.Time, region string, stype serviceTy
 func getSignature(signingKey []byte, stringToSign string) string {
 	return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
 }
 
 // Check to see if Policy is signed correctly.
-func doesPolicySignatureMatch(formValues http.Header) APIErrorCode {
+func doesPolicySignatureMatch(formValues http.Header) (auth.Credentials, APIErrorCode) {
 	// For SignV2 - Signature field will be valid
 	if _, ok := formValues["Signature"]; ok {
 		return doesPolicySignatureV2Match(formValues)
 	}
 	return doesPolicySignatureV4Match(formValues)
@@ -167,38 +168,38 @@ func compareSignatureV4(sig1, sig2 string) bool {
 }
 
 // doesPolicySignatureMatch - Verify query headers with post policy
 //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
 // returns ErrNone if the signature matches.
-func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
+func doesPolicySignatureV4Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
 	// Server region.
 	region := globalServerRegion
 
 	// Parse credential tag.
 	credHeader, s3Err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3)
 	if s3Err != ErrNone {
-		return s3Err
+		return auth.Credentials{}, s3Err
 	}
 
 	cred, _, s3Err := checkKeyValid(credHeader.accessKey)
 	if s3Err != ErrNone {
-		return s3Err
+		return cred, s3Err
 	}
 
 	// Get signing key.
 	signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date, credHeader.scope.region, serviceS3)
 
 	// Get signature.
 	newSignature := getSignature(signingKey, formValues.Get("Policy"))
 
 	// Verify signature.
 	if !compareSignatureV4(newSignature, formValues.Get(xhttp.AmzSignature)) {
-		return ErrSignatureDoesNotMatch
+		return cred, ErrSignatureDoesNotMatch
 	}
 
 	// Success.
-	return ErrNone
+	return cred, ErrNone
 }
 
 // doesPresignedSignatureMatch - Verify query headers with presigned signature
 //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
 // returns ErrNone if the signature matches.
diff --git a/cmd/signature-v4_test.go b/cmd/signature-v4_test.go
index b7dc57831..f47d784ec 100644
--- a/cmd/signature-v4_test.go
+++ b/cmd/signature-v4_test.go
@@ -82,11 +82,11 @@ func TestDoesPolicySignatureMatch(t *testing.T) {
 		},
 	}
 
 	// Run each test case individually.
 	for i, testCase := range testCases {
-		code := doesPolicySignatureMatch(testCase.form)
+		_, code := doesPolicySignatureMatch(testCase.form)
 		if code != testCase.expected {
 			t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(code))
 		}
 	}
 }

Vulnerable code: 	claims, _ := getClaimsFromToken(r, getSessionToken(r))
func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, error) {
			logger.LogIf(r.Context(), err, logger.Application)
	claims, err := getClaimsFromToken(r, token)
	"path/filepath"
		claims, _ := getClaimsFromToken(r, getSessionToken(r))
	// Make sure that the URL  does not contain object name.
	if bucket != filepath.Clean(resource[1:]) {

	errCode := doesPolicySignatureMatch(formValues)

func doesPolicySignatureV2Match(formValues http.Header) APIErrorCode {
	cred := globalActiveCred
		return s3Err
		return ErrSignatureDoesNotMatch
	return ErrNone
		errCode := doesPolicySignatureV2Match(formValues)
func doesPolicySignatureMatch(formValues http.Header) APIErrorCode {
func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
		return s3Err
		return s3Err
		return ErrSignatureDoesNotMatch
	return ErrNone
		code := doesPolicySignatureMatch(testCase.form)
Fixed code: 	claims, _ := getClaimsFromToken(getSessionToken(r))
func getClaimsFromToken(token string) (map[string]interface{}, error) {
			logger.LogIf(GlobalContext, err, logger.Application)
	claims, err := getClaimsFromToken(token)
	"crypto/subtle"
		claims, _ := getClaimsFromToken(getSessionToken(r))


	// Make sure that the URL does not contain object name.
	if bucket != path.Clean(resource[1:]) {
	cred, errCode := doesPolicySignatureMatch(formValues)
	// Once signature is validated, check if the user has
	// explicit permissions for the user.
	{
		token := formValues.Get(xhttp.AmzSecurityToken)
		if token != "" && cred.AccessKey == "" {
			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL, guessIsBrowserReq(r))
			return
		}

		if cred.IsServiceAccount() && token == "" {
			token = cred.SessionToken
		}

		if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL, guessIsBrowserReq(r))
			return
		}

		// Extract claims if any.
		claims, err := getClaimsFromToken(token)
		if err != nil {
			writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
			return
		}

		if !globalIAMSys.IsAllowed(iampolicy.Args{
			AccountName:     cred.AccessKey,
			Action:          iampolicy.PutObjectAction,
			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
			BucketName:      bucket,
			ObjectName:      object,
			IsOwner:         globalActiveCred.AccessKey == cred.AccessKey,
			Claims:          claims,
		}) {
			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL, guessIsBrowserReq(r))
			return
		}
	}

func doesPolicySignatureV2Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
		return cred, s3Err
		return cred, ErrSignatureDoesNotMatch
	return cred, ErrNone
		_, errCode := doesPolicySignatureV2Match(formValues)
	"github.com/minio/minio/pkg/auth"
func doesPolicySignatureMatch(formValues http.Header) (auth.Credentials, APIErrorCode) {
func doesPolicySignatureV4Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
		return auth.Credentials{}, s3Err
		return cred, s3Err
		return cred, ErrSignatureDoesNotMatch
	return cred, ErrNone
		_, code := doesPolicySignatureMatch(testCase.form)