CVE-2021-43858 (Go, published 2021-12-27T22:15Z)
Description: value: MinIO is a Kubernetes native application for cloud storage. Prior to version `RELEASE.2021-12-27T07-23-18Z`, a malicious client can hand-craft an HTTP API call that allows for updating policy for a user and gaining higher privileges. The patch in version `RELEASE.2021-12-27T07-23-18Z` changes the accepted request body type and removes the ability to apply policy changes through this API. There is a workaround for this vulnerability: Changing passwords can be disabled by adding an explicit `Deny` rule to disable the API for users.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/minio/minio
Files changed:
cmd/admin-handlers-users.go
cmd/admin-handlers-users_test.go
cmd/iam-store.go
cmd/iam.go
cmd/signature-v4-utils_test.go
go.mod
go.sum
Commit message: Fix user privilege escalation bug (#13976)

The AddUser() API endpoint was accepting a policy field. 
This API is used to update a user's secret key and account 
status, and allows a regular user to update their own secret key. 

The policy update is also applied though does not appear to 
be used by any existing client-side functionality.

This fix changes the accepted request body type and removes 
the ability to apply policy changes as that is possible via the 
policy set API.

NOTE: Changing passwords can be disabled as a workaround
for this issue by adding an explicit "Deny" rule to disable the API
for users.

Vulnerable code:
	var uinfo madmin.UserInfo
	if err = json.Unmarshal(configBytes, &uinfo); err != nil {
	if err = globalIAMSys.CreateUser(ctx, accessKey, uinfo); err != nil {
func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
		SecretKey: uinfo.SecretKey,
			if uinfo.Status == madmin.AccountEnabled {
	// Set policy if specified.
	if uinfo.PolicyName != "" {
		policy := uinfo.PolicyName
		// Handle policy mapping set/update
		mp := newMappedPolicy(policy)
		for _, p := range mp.toSlice() {
			if _, found := cache.iamPolicyDocsMap[policy]; !found {
				logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p))
				return errNoSuchPolicy
			}
		}

		if err := store.saveMappedPolicy(ctx, accessKey, regUser, false, mp); err != nil {
			return err
		}
		cache.iamUserPolicyMap[accessKey] = mp
	}

func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
	if !auth.IsSecretKeyValid(uinfo.SecretKey) {
	err := sys.store.AddUser(ctx, accessKey, uinfo)
	globalIAMSys.CreateUser(ctx, ucreds.AccessKey, madmin.UserInfo{
	github.com/minio/madmin-go v1.1.18
github.com/minio/madmin-go v1.1.18 h1:1jXRb9LTiXqbZWBjXYmlqI5eCWsyZlprrI5CEVQjjqY=
github.com/minio/madmin-go v1.1.18/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=

Fixed code:
	var ureq madmin.AddOrUpdateUserReq
	if err = json.Unmarshal(configBytes, &ureq); err != nil {
	if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io/ioutil"
	"net/http"
	"net/url"
	"github.com/minio/minio-go/v7/pkg/s3utils"
	"github.com/minio/minio-go/v7/pkg/signer"
				suite.TestUserPolicyEscalationBug(c)
	// 3.10. Check that user's password can be updated.
	_, newSecretKey := mustGenerateCredentials(c)
	err = s.adm.SetUser(ctx, accessKey, newSecretKey, madmin.AccountEnabled)
	if err != nil {
		c.Fatalf("Unable to update user's secret key: %v", err)
	}
	// 3.10.1 Check that old password no longer works.
	err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
	if err == nil {
		c.Fatalf("user was unexpectedly able to create bucket with bad password!")
	}
	// 3.10.2 Check that new password works.
	client = s.getUserClient(c, accessKey, newSecretKey, "")
	err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
	if err != nil {
		c.Fatalf("user could not create bucket: %v", err)
	}

func (s *TestSuiteIAM) TestUserPolicyEscalationBug(c *check) {
	ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
	defer cancel()

	bucket := getRandomBucketName()
	err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
	if err != nil {
		c.Fatalf("bucket creat error: %v", err)
	}

	// 2. Create a user, associate policy and verify access
	accessKey, secretKey := mustGenerateCredentials(c)
	err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
	if err != nil {
		c.Fatalf("Unable to set user: %v", err)
	}
	// 2.1 check that user does not have any access to the bucket
	uClient := s.getUserClient(c, accessKey, secretKey, "")
	c.mustNotListObjects(ctx, uClient, bucket)

	// 2.2 create and associate policy to user
	policy := "mypolicy-test-user-update"
	policyBytes := []byte(fmt.Sprintf(`{
 "Version": "2012-10-17",
 "Statement": [
  {
   "Effect": "Allow",
   "Action": [
    "s3:PutObject",
    "s3:GetObject",
    "s3:ListBucket"
   ],
   "Resource": [
    "arn:aws:s3:::%s/*"
   ]
  }
 ]
}`, bucket))
	err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
	if err != nil {
		c.Fatalf("policy add error: %v", err)
	}
	err = s.adm.SetPolicy(ctx, policy, accessKey, false)
	if err != nil {
		c.Fatalf("Unable to set policy: %v", err)
	}
	// 2.3 check user has access to bucket
	c.mustListObjects(ctx, uClient, bucket)
	// 2.3 check that user cannot delete the bucket
	err = uClient.RemoveBucket(ctx, bucket)
	if err == nil || err.Error() != "Access Denied." {
		c.Fatalf("bucket was deleted unexpectedly or got unexpected err: %v", err)
	}

	// 3. Craft a request to update the user's permissions
	ep := s.adm.GetEndpointURL()
	urlValue := url.Values{}
	urlValue.Add("accessKey", accessKey)
	u, err := url.Parse(fmt.Sprintf("%s://%s/minio/admin/v3/add-user?%s", ep.Scheme, ep.Host, s3utils.QueryEncode(urlValue)))
	if err != nil {
		c.Fatalf("unexpected url parse err: %v", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), nil)
	if err != nil {
		c.Fatalf("unexpected new request err: %v", err)
	}
	reqBodyArg := madmin.UserInfo{
		SecretKey:  secretKey,
		PolicyName: "consoleAdmin",
		Status:     madmin.AccountEnabled,
	}
	buf, err := json.Marshal(reqBodyArg)
	if err != nil {
		c.Fatalf("unexpected json encode err: %v", err)
	}
	buf, err = madmin.EncryptData(secretKey, buf)
	if err != nil {
		c.Fatalf("unexpected encryption err: %v", err)
	}

	req.ContentLength = int64(len(buf))
	sum := sha256.Sum256(buf)
	req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
	req.Body = ioutil.NopCloser(bytes.NewReader(buf))
	req = signer.SignV4(*req, accessKey, secretKey, "", "")

	// 3.1 Execute the request.
	resp, err := s.TestSuiteCommon.client.Do(req)
	if err != nil {
		c.Fatalf("unexpected request err: %v", err)
	}
	if resp.StatusCode != 200 {
		c.Fatalf("got unexpected response: %#v\n", resp)
	}

	// 3.2 check that user cannot delete the bucket
	err = uClient.RemoveBucket(ctx, bucket)
	if err == nil || err.Error() != "Access Denied." {
		c.Fatalf("User was able to escalate privileges (Err=%v)!", err)
	}
}

func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
		SecretKey: ureq.SecretKey,
			if ureq.Status == madmin.AccountEnabled {
func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
	if !auth.IsSecretKeyValid(ureq.SecretKey) {
	err := sys.store.AddUser(ctx, accessKey, ureq)
	globalIAMSys.CreateUser(ctx, ucreds.AccessKey, madmin.AddOrUpdateUserReq{
	github.com/minio/madmin-go v1.1.20
github.com/minio/madmin-go v1.1.20 h1:jig4gJi0CD+FYz+Cnd+TNo0oqhNaZcLmfUqNl5b46Eo=
github.com/minio/madmin-go v1.1.20/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=