Headline
CVE-2021-21362: fix: missing user policy enforcement in PostPolicyHandler (#11682) · minio/minio@039f59b
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.
@@ -17,6 +17,7 @@ package cmd
import ( “crypto/subtle” “encoding/base64” “encoding/xml” “fmt” @@ -25,7 +26,6 @@ import ( “net/textproto” “net/url” “path” “path/filepath” “sort” “strconv” “strings” @@ -337,7 +337,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
// err will be nil here as we already called this function // earlier in this request. claims, _ := getClaimsFromToken(r, getSessionToken®) claims, _ := getClaimsFromToken(getSessionToken®) n := 0 // Use the following trick to filter in place // https://github.com/golang/go/wiki/SliceTricks#filter-in-place @@ -797,13 +797,15 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq®) return }
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames) if err != nil { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq®) 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®) return } @@ -846,7 +848,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h 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 @@ -866,12 +867,51 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h }
// Verify policy signature. errCode := doesPolicySignatureMatch(formValues) cred, errCode := doesPolicySignatureMatch(formValues) if errCode != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq®) 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®) 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®) return }
// Extract claims if any. claims, err := getClaimsFromToken(token) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq®) 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®) return } }
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get(“Policy”)) if err != nil { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL, guessIsBrowserReq®)