CVE-2023-46738: Allocation of Resources Without Limits or Throttling (CWE-770)
value: CubeFS is an open-source cloud-native file storage system. A security vulnerability was found in CubeFS HandlerNode in versions prior to 3.3.1 that could allow authenticated users to send maliciously-crafted requests that would crash the ObjectNode and deny other users from using it. The root cause was improper handling of incoming HTTP requests that could allow an attacker to control the ammount of memory that the ObjectNode would allocate. A malicious request could make the ObjectNode allocate more memory that the machine had available, and the attacker could exhaust memory by way of a single malicious request. An attacker would need to be authenticated in order to invoke the vulnerable code with their malicious request and have permissions to delete objects. In addition, the attacker would need to know the names of existing buckets of the CubeFS deployment - otherwise the request would be rejected before it reached the vulnerable code. As such, the most likely attacker is an inside user or an attacker that has breached the account of an existing user in the cluster. The issue has been patched in v3.3.1. There is no other mitigation besides upgrading.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 6.5
Published: 2024-01-03T16:15Z
Repository: https://github.com/cubefs/cubefs
Language: Go
Files changed: objectnode/api_handler_bucket.go, objectnode/api_handler_multipart.go, objectnode/api_handler_object.go, objectnode/const.go, objectnode/lifecycle_handler.go
Commit date: 2023-10-20 17:06:26 +0800
Commit message: fix(objectnode): fix read body entirely into memory and without an upper boundary

Signed-off-by: tangdeyi <tangdeyi@oppo.com>

Diff: diff --git a/objectnode/api_handler_bucket.go b/objectnode/api_handler_bucket.go
index 1fac5143..297cad8a 100644
--- a/objectnode/api_handler_bucket.go
+++ b/objectnode/api_handler_bucket.go
@@ -21,11 +21,10 @@ import (
 	"encoding/xml"
 	"io"
 	"io/ioutil"
 	"net/http"
 	"regexp"
-	"strconv"
 	"strings"
 
 	"github.com/cubefs/cubefs/proto"
 	"github.com/cubefs/cubefs/util/log"
 )
@@ -84,34 +83,33 @@ func (o *ObjectNode) createBucketHandler(w http.ResponseWriter, r *http.Request)
 	if err = rateLimit.AcquireLimitResource(userInfo.UserID, param.apiName); err != nil {
 		return
 	}
 	defer rateLimit.ReleaseLimitResource(userInfo.UserID, param.apiName)
 
-	// get LocationConstraint if any
-	contentLenStr := r.Header.Get(ContentLength)
-	if contentLen, errConv := strconv.Atoi(contentLenStr); errConv == nil && contentLen > 0 {
-		var requestBytes []byte
-		requestBytes, err = ioutil.ReadAll(r.Body)
-		if err != nil && err != io.EOF {
-			log.LogErrorf("createBucketHandler: read request body fail: requestID(%v) err(%v)", GetRequestID(r), err)
-			return
-		}
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
+	requestBytes, err := ioutil.ReadAll(r.Body)
+	if err != nil && err != io.EOF {
+		log.LogErrorf("createBucketHandler: read request body fail: requestID(%v) err(%v)", GetRequestID(r), err)
+		return
+	}
 
-		createBucketRequest := &CreateBucketRequest{}
-		err = UnmarshalXMLEntity(requestBytes, createBucketRequest)
-		if err != nil {
-			log.LogErrorf("createBucketHandler: unmarshal xml fail: requestID(%v) err(%v)",
-				GetRequestID(r), err)
-			errorCode = InvalidArgument
-			return
-		}
-		if createBucketRequest.LocationConstraint != o.region {
-			log.LogErrorf("createBucketHandler: location constraint not match the service: requestID(%v) LocationConstraint(%v) region(%v)",
-				GetRequestID(r), createBucketRequest.LocationConstraint, o.region)
-			errorCode = InvalidLocationConstraint
-			return
-		}
+	createBucketRequest := &CreateBucketRequest{}
+	err = UnmarshalXMLEntity(requestBytes, createBucketRequest)
+	if err != nil {
+		log.LogErrorf("createBucketHandler: unmarshal xml fail: requestID(%v) err(%v)",
+			GetRequestID(r), err)
+		errorCode = InvalidArgument
+		return
+	}
+	if createBucketRequest.LocationConstraint != o.region {
+		log.LogErrorf("createBucketHandler: location constraint not match the service: requestID(%v) LocationConstraint(%v) region(%v)",
+			GetRequestID(r), createBucketRequest.LocationConstraint, o.region)
+		errorCode = InvalidLocationConstraint
+		return
 	}
 
 	var acl *AccessControlPolicy
 	if acl, err = ParseACL(r, userInfo.UserID, false, false); err != nil {
 		log.LogErrorf("createBucketHandler: parse acl fail: requestID(%v) err(%v)", GetRequestID(r), err)
@@ -395,10 +393,14 @@ func (o *ObjectNode) putBucketTaggingHandler(w http.ResponseWriter, r *http.Requ
 	if err = rateLimit.AcquireLimitResource(vol.owner, param.apiName); err != nil {
 		return
 	}
 	defer rateLimit.ReleaseLimitResource(vol.owner, param.apiName)
 
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
 	var body []byte
 	if body, err = ioutil.ReadAll(r.Body); err != nil {
 		log.LogErrorf("putBucketTaggingHandler: read request body data fail: requestID(%v) err(%v)",
 			GetRequestID(r), err)
 		errorCode = InvalidArgument
diff --git a/objectnode/api_handler_multipart.go b/objectnode/api_handler_multipart.go
index 00adad35..e799ee81 100644
--- a/objectnode/api_handler_multipart.go
+++ b/objectnode/api_handler_multipart.go
@@ -649,12 +649,15 @@ func (o *ObjectNode) completeMultipartUploadHandler(w http.ResponseWriter, r *ht
 		return
 	}
 	defer rateLimit.ReleaseLimitResource(vol.owner, param.apiName)
 
 	// get uploaded part info in request
-	var requestBytes []byte
-	requestBytes, err = ioutil.ReadAll(r.Body)
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
+	requestBytes, err := ioutil.ReadAll(r.Body)
 	if err != nil && err != io.EOF {
 		log.LogErrorf("completeMultipartUploadHandler: read request body fail: requestID(%v) err(%v)",
 			GetRequestID(r), err)
 		return
 	}
diff --git a/objectnode/api_handler_object.go b/objectnode/api_handler_object.go
index 76e7d640..05e1a712 100644
--- a/objectnode/api_handler_object.go
+++ b/objectnode/api_handler_object.go
@@ -555,12 +555,15 @@ func (o *ObjectNode) deleteObjectsHandler(w http.ResponseWriter, r *http.Request
 	if requestMD5 == "" {
 		errorCode = MissingContentMD5
 		return
 	}
 
-	var bytes []byte
-	bytes, err = ioutil.ReadAll(r.Body)
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
+	bytes, err := ioutil.ReadAll(r.Body)
 	if err != nil {
 		log.LogErrorf("deleteObjectsHandler: read request body fail: requestID(%v) volume(%v) err(%v)",
 			GetRequestID(r), param.Bucket(), err)
 		errorCode = UnexpectedContent
 		return
@@ -1550,10 +1553,14 @@ func (o *ObjectNode) putObjectTaggingHandler(w http.ResponseWriter, r *http.Requ
 	if err = rateLimit.AcquireLimitResource(vol.owner, param.apiName); err != nil {
 		return
 	}
 	defer rateLimit.ReleaseLimitResource(vol.owner, param.apiName)
 
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
 	var requestBody []byte
 	if requestBody, err = ioutil.ReadAll(r.Body); err != nil {
 		log.LogErrorf("putObjectTaggingHandler: read request body data fail: requestID(%v) err(%v)",
 			GetRequestID(r), err)
 		errorCode = InvalidArgument
@@ -1658,10 +1665,14 @@ func (o *ObjectNode) putObjectXAttrHandler(w http.ResponseWriter, r *http.Reques
 	if err = rateLimit.AcquireLimitResource(vol.owner, param.apiName); err != nil {
 		return
 	}
 	defer rateLimit.ReleaseLimitResource(vol.owner, param.apiName)
 
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
 	var requestBody []byte
 	if requestBody, err = ioutil.ReadAll(r.Body); err != nil {
 		errorCode = &ErrorCode{
 			ErrorCode:    "BadRequest",
 			ErrorMessage: err.Error(),
@@ -1960,5 +1971,23 @@ func GetContentLength(r *http.Request) int64 {
 			return length
 		}
 	}
 	return r.ContentLength
 }
+
+func VerifyContentLength(r *http.Request, bodyLimit int64) (int64, *ErrorCode) {
+	dcl := r.Header.Get(HeaderNameXAmzDecodedContentLength)
+	var length = r.ContentLength
+	if dcl != "" {
+		l, err := strconv.ParseInt(dcl, 10, 64)
+		if err == nil {
+			length = l
+		}
+	}
+	if length > bodyLimit {
+		return 0, EntityTooLarge
+	}
+	if length <= 0 {
+		return 0, MissingContentLength
+	}
+	return length, nil
+}
diff --git a/objectnode/const.go b/objectnode/const.go
index 5c18a478..e71a82a1 100644
--- a/objectnode/const.go
+++ b/objectnode/const.go
@@ -131,10 +131,11 @@ const (
 const (
 	MaxKeys        = 1000
 	MaxParts       = 1000
 	MaxUploads     = 1000
 	SinglePutLimit = 5 * 1 << 30 // 5G
+	BodyLimit      = 1 << 20
 )
 
 const (
 	StorageClassStandard = "STANDARD"
 )
diff --git a/objectnode/lifecycle_handler.go b/objectnode/lifecycle_handler.go
index a0d0926a..e8838c7c 100644
--- a/objectnode/lifecycle_handler.go
+++ b/objectnode/lifecycle_handler.go
@@ -104,10 +104,14 @@ func (o *ObjectNode) putBucketLifecycleConfigurationHandler(w http.ResponseWrite
 	if _, err = o.vm.Volume(param.Bucket()); err != nil {
 		errorCode = NoSuchBucket
 		return
 	}
 
+	_, errorCode = VerifyContentLength(r, BodyLimit)
+	if errorCode != nil {
+		return
+	}
 	var requestBody []byte
 	if requestBody, err = ioutil.ReadAll(r.Body); err != nil && err != io.EOF {
 		log.LogErrorf("putBucketLifecycle failed: read request body data err: requestID(%v) err(%v)", GetRequestID(r), err)
 		errorCode = &ErrorCode{
 			ErrorCode:    http.StatusText(http.StatusBadRequest),

Vulnerable code: 	"strconv"
	// get LocationConstraint if any
	contentLenStr := r.Header.Get(ContentLength)
	if contentLen, errConv := strconv.Atoi(contentLenStr); errConv == nil && contentLen > 0 {
		var requestBytes []byte
		requestBytes, err = ioutil.ReadAll(r.Body)
		if err != nil && err != io.EOF {
			log.LogErrorf("createBucketHandler: read request body fail: requestID(%v) err(%v)", GetRequestID(r), err)
			return
		}
		createBucketRequest := &CreateBucketRequest{}
		err = UnmarshalXMLEntity(requestBytes, createBucketRequest)
		if err != nil {
			log.LogErrorf("createBucketHandler: unmarshal xml fail: requestID(%v) err(%v)",
				GetRequestID(r), err)
			errorCode = InvalidArgument
			return
		}
		if createBucketRequest.LocationConstraint != o.region {
			log.LogErrorf("createBucketHandler: location constraint not match the service: requestID(%v) LocationConstraint(%v) region(%v)",
				GetRequestID(r), createBucketRequest.LocationConstraint, o.region)
			errorCode = InvalidLocationConstraint
			return
		}
	var requestBytes []byte
	requestBytes, err = ioutil.ReadAll(r.Body)
	var bytes []byte
	bytes, err = ioutil.ReadAll(r.Body)
Fixed code: 	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}
	requestBytes, err := ioutil.ReadAll(r.Body)
	if err != nil && err != io.EOF {
		log.LogErrorf("createBucketHandler: read request body fail: requestID(%v) err(%v)", GetRequestID(r), err)
		return
	}
	createBucketRequest := &CreateBucketRequest{}
	err = UnmarshalXMLEntity(requestBytes, createBucketRequest)
	if err != nil {
		log.LogErrorf("createBucketHandler: unmarshal xml fail: requestID(%v) err(%v)",
			GetRequestID(r), err)
		errorCode = InvalidArgument
		return
	}
	if createBucketRequest.LocationConstraint != o.region {
		log.LogErrorf("createBucketHandler: location constraint not match the service: requestID(%v) LocationConstraint(%v) region(%v)",
			GetRequestID(r), createBucketRequest.LocationConstraint, o.region)
		errorCode = InvalidLocationConstraint
		return
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}
	requestBytes, err := ioutil.ReadAll(r.Body)
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}
	bytes, err := ioutil.ReadAll(r.Body)
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}

func VerifyContentLength(r *http.Request, bodyLimit int64) (int64, *ErrorCode) {
	dcl := r.Header.Get(HeaderNameXAmzDecodedContentLength)
	var length = r.ContentLength
	if dcl != "" {
		l, err := strconv.ParseInt(dcl, 10, 64)
		if err == nil {
			length = l
		}
	}
	if length > bodyLimit {
		return 0, EntityTooLarge
	}
	if length <= 0 {
		return 0, MissingContentLength
	}
	return length, nil
}
	BodyLimit      = 1 << 20
	_, errorCode = VerifyContentLength(r, BodyLimit)
	if errorCode != nil {
		return
	}