CVE-2024-24747 (Go)

MinIO is a High Performance Object Storage. When someone creates an access key, it inherits the permissions of the parent key. Not only for `s3:*` actions, but also `admin:*` actions. Which means unless somewhere above in the access-key hierarchy, the `admin` rights are denied, access keys will be able to simply override their own `s3` permissions to something more permissive. The vulnerability is fixed in RELEASE.2024-01-31T20-20-33Z.

Weakness:
CWE-285: name: Improper Authorization
description: The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.
CWE-250: name: Execution with Unnecessary Privileges
description: The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.
CWE-269: name: Improper Privilege Management
description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

Repository: https://github.com/minio/minio

Vulnerable code:
file_path: cmd/admin-handlers-users.go
snippet: func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get current object layer instance.
	objectAPI := newObjectLayerFn()
	if objectAPI == nil || globalNotificationSys == nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
		return
	}

	cred, owner, s3Err := validateAdminSignature(ctx, r, "")
	if s3Err != ErrNone {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
		return
	}

	accessKey := mux.Vars(r)["accessKey"]
	if accessKey == "" {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
		return
	}

	svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
	if err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}

	if !globalIAMSys.IsAllowed(policy.Args{
		AccountName:     cred.AccessKey,
		Groups:          cred.Groups,
		Action:          policy.UpdateServiceAccountAdminAction,
		ConditionValues: getConditionValues(r, "", cred),
		IsOwner:         owner,
		Claims:          cred.Claims,
	}) {
		requestUser := cred.AccessKey
		if cred.ParentUser != "" {
			requestUser = cred.ParentUser
		}

		if requestUser != svcAccount.ParentUser {
			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
			return
		}
	}

	password := cred.SecretKey
	reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
	if err != nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
		return
	}

	var updateReq madmin.UpdateServiceAccountReq
	if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
		return
	}

	if err := updateReq.Validate(); err != nil {
		// Since this validation would happen client side as well, we only send
		// a generic error message here.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
		return
	}

	var sp *policy.Policy
	if len(updateReq.NewPolicy) > 0 {
		sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
		if err != nil {
			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
			return
		}
		if sp.Version == "" && len(sp.Statements) == 0 {
			sp = nil
		}
	}
	opts := updateServiceAccountOpts{
		secretKey:     updateReq.NewSecretKey,
		status:        updateReq.NewStatus,
		name:          updateReq.NewName,
		description:   updateReq.NewDescription,
		expiration:    updateReq.NewExpiration,
		sessionPolicy: sp,
	}
	updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
	if err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}

	// Call site replication hook - non-root user accounts are replicated.
	if svcAccount.ParentUser != globalActiveCred.AccessKey {
		logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
			Type: madmin.SRIAMItemSvcAcc,
			SvcAccChange: &madmin.SRSvcAccChange{
				Update: &madmin.SRSvcAccUpdate{
					AccessKey:     accessKey,
					SecretKey:     opts.secretKey,
					Status:        opts.status,
					Name:          opts.name,
					Description:   opts.description,
					SessionPolicy: updateReq.NewPolicy,
					Expiration:    updateReq.NewExpiration,
				},
			},
			UpdatedAt: updatedAt,
		}))
	}

	writeSuccessNoContent(w)
}

Fixed code:
file_path: cmd/admin-handlers-users.go
snippet: func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get current object layer instance.
	objectAPI := newObjectLayerFn()
	if objectAPI == nil || globalNotificationSys == nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
		return
	}

	cred, owner, s3Err := validateAdminSignature(ctx, r, "")
	if s3Err != ErrNone {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
		return
	}

	accessKey := mux.Vars(r)["accessKey"]
	if accessKey == "" {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
		return
	}

	svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
	if err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}

	// Permission checks:
	//
	// 1. Any type of account (i.e. access keys (previously/still called service
	// accounts), STS accounts, internal IDP accounts, etc) with the
	// policy.UpdateServiceAccountAdminAction permission can update any service
	// account.
	//
	// 2. We would like to let a user update their own access keys, however it
	// is currently blocked pending a re-design. Users are still able to delete
	// and re-create them.
	if !globalIAMSys.IsAllowed(policy.Args{
		AccountName:     cred.AccessKey,
		Groups:          cred.Groups,
		Action:          policy.UpdateServiceAccountAdminAction,
		ConditionValues: getConditionValues(r, "", cred),
		IsOwner:         owner,
		Claims:          cred.Claims,
	}) {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
		return
	}

	password := cred.SecretKey
	reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
	if err != nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
		return
	}

	var updateReq madmin.UpdateServiceAccountReq
	if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
		return
	}

	if err := updateReq.Validate(); err != nil {
		// Since this validation would happen client side as well, we only send
		// a generic error message here.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
		return
	}

	var sp *policy.Policy
	if len(updateReq.NewPolicy) > 0 {
		sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
		if err != nil {
			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
			return
		}
		if sp.Version == "" && len(sp.Statements) == 0 {
			sp = nil
		}
	}
	opts := updateServiceAccountOpts{
		secretKey:     updateReq.NewSecretKey,
		status:        updateReq.NewStatus,
		name:          updateReq.NewName,
		description:   updateReq.NewDescription,
		expiration:    updateReq.NewExpiration,
		sessionPolicy: sp,
	}
	updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
	if err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}

	// Call site replication hook - non-root user accounts are replicated.
	if svcAccount.ParentUser != globalActiveCred.AccessKey {
		logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
			Type: madmin.SRIAMItemSvcAcc,
			SvcAccChange: &madmin.SRSvcAccChange{
				Update: &madmin.SRSvcAccUpdate{
					AccessKey:     accessKey,
					SecretKey:     opts.secretKey,
					Status:        opts.status,
					Name:          opts.name,
					Description:   opts.description,
					SessionPolicy: updateReq.NewPolicy,
					Expiration:    updateReq.NewExpiration,
				},
			},
			UpdatedAt: updatedAt,
		}))
	}

	writeSuccessNoContent(w)
}