CVE-2018-1000538: Allocation of File Descriptors or Handles Without Limits or Throttling (CWE-774)
value: Minio Inc. Minio S3 server version prior to RELEASE.2018-05-16T23-35-33Z contains a Allocation of Memory Without Limits or Throttling (similar to CWE-774) vulnerability in write-to-RAM that can result in Denial of Service. This attack appear to be exploitable via Sending V4-(pre)signed requests with large bodies . This vulnerability appears to have been fixed in after commit 9c8b7306f55f2c8c0a5c7cea9a8db9d34be8faa7.
CWE description: The product allocates file descriptors or handles on behalf of an actor without imposing any restrictions on how many descriptors can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2018-06-26T16:29Z
Repository: https://github.com/minio/minio
Language: Go
Files changed: cmd/auth-handler.go, cmd/auth-handler_test.go, pkg/hash/reader.go
Commit date: 2018-05-18 11:27:25 -0700
Commit message: security: fix write-to-RAM DoS vulnerability (#5957)

This commit fixes a DoS vulnerability for certain APIs using
signature V4 by verifying the content-md5 and/or content-sha56 of
the request body in a streaming mode.

The issue was caused by reading the entire body of the request into
memory to verify the content-md5 or content-sha56 checksum if present.

The vulnerability could be exploited by either replaying a V4 request
(in the 15 min time frame) or sending a V4 presigned request with a
large body.
Diff: diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go
index a09184a6..0298ba8e 100644
--- a/cmd/auth-handler.go
+++ b/cmd/auth-handler.go
@@ -26,10 +26,11 @@ import (
 	"io/ioutil"
 	"net/http"
 	"strings"
 
 	"github.com/minio/minio/cmd/logger"
+	"github.com/minio/minio/pkg/hash"
 	"github.com/minio/minio/pkg/policy"
 )
 
 // Verify if request has JWT.
 func isRequestJWT(r *http.Request) bool {
@@ -207,63 +208,49 @@ func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode)
 	}
 }
 
 // Verify if request has valid AWS Signature Version '4'.
 func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
-	if r == nil {
-		return ErrInternalError
-	}
-
 	if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
 		return errCode
 	}
 
-	payload, err := ioutil.ReadAll(r.Body)
-	if err != nil {
-		logger.LogIf(context.Background(), err)
-		return ErrInternalError
-	}
-
-	// Populate back the payload.
-	r.Body = ioutil.NopCloser(bytes.NewReader(payload))
-
-	// Verify Content-Md5, if payload is set.
-	if clntMD5B64, ok := r.Header["Content-Md5"]; ok {
-		if clntMD5B64[0] == "" {
+	var (
+		err                       error
+		contentMD5, contentSHA256 []byte
+	)
+	// Extract 'Content-Md5' if present.
+	if _, ok := r.Header["Content-Md5"]; ok {
+		contentMD5, err = base64.StdEncoding.Strict().DecodeString(r.Header.Get("Content-Md5"))
+		if err != nil || len(contentMD5) == 0 {
 			return ErrInvalidDigest
 		}
-		md5Sum, err := base64.StdEncoding.Strict().DecodeString(clntMD5B64[0])
-		if err != nil {
-			return ErrInvalidDigest
-		}
-		if !bytes.Equal(md5Sum, getMD5Sum(payload)) {
-			return ErrBadDigest
-		}
 	}
 
-	if skipContentSha256Cksum(r) {
-		return ErrNone
-	}
-
-	// Verify that X-Amz-Content-Sha256 Header == sha256(payload)
-	// If X-Amz-Content-Sha256 header is not sent then we don't calculate/verify sha256(payload)
-	sumHex, ok := r.Header["X-Amz-Content-Sha256"]
-	if isRequestPresignedSignatureV4(r) {
-		sumHex, ok = r.URL.Query()["X-Amz-Content-Sha256"]
-	}
-	if ok {
-		if sumHex[0] == "" {
-			return ErrContentSHA256Mismatch
-		}
-		sum, err := hex.DecodeString(sumHex[0])
-		if err != nil {
-			return ErrContentSHA256Mismatch
+	// Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
+	// Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
+	if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
+		if sha256Sum, ok := r.URL.Query()["X-Amz-Content-Sha256"]; ok && len(sha256Sum) > 0 {
+			contentSHA256, err = hex.DecodeString(sha256Sum[0])
+			if err != nil {
+				return ErrContentSHA256Mismatch
+			}
 		}
-		if !bytes.Equal(sum, getSHA256Sum(payload)) {
+	} else if _, ok := r.Header["X-Amz-Content-Sha256"]; !skipSHA256 && ok {
+		contentSHA256, err = hex.DecodeString(r.Header.Get("X-Amz-Content-Sha256"))
+		if err != nil || len(contentSHA256) == 0 {
 			return ErrContentSHA256Mismatch
 		}
 	}
+
+	// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
+	// The verification happens implicit during reading.
+	reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256))
+	if err != nil {
+		return toAPIErrorCode(err)
+	}
+	r.Body = ioutil.NopCloser(reader)
 	return ErrNone
 }
 
 // authHandler - handles all the incoming authorization headers and validates them if possible.
 type authHandler struct {
diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go
index d88b913e..811103dd 100644
--- a/cmd/auth-handler_test.go
+++ b/cmd/auth-handler_test.go
@@ -17,10 +17,11 @@
 package cmd
 
 import (
 	"bytes"
 	"io"
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
 	"testing"
 	"time"
@@ -359,12 +360,10 @@ func TestIsReqAuthenticated(t *testing.T) {
 	// List of test cases for validating http request authentication.
 	testCases := []struct {
 		req     *http.Request
 		s3Error APIErrorCode
 	}{
-		// When request is nil, internal error is returned.
-		{nil, ErrInternalError},
 		// When request is unsigned, access denied is returned.
 		{mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
 		// Empty Content-Md5 header.
 		{mustNewSignedEmptyMD5Request("PUT", "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrInvalidDigest},
 		// Short Content-Md5 header.
@@ -374,13 +373,15 @@ func TestIsReqAuthenticated(t *testing.T) {
 		// When request is properly signed, error is none.
 		{mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrNone},
 	}
 
 	// Validates all testcases.
-	for _, testCase := range testCases {
+	for i, testCase := range testCases {
 		if s3Error := isReqAuthenticated(testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error {
-			t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
+			if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(err) != testCase.s3Error {
+				t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %d)", i, testCase.s3Error, s3Error, toAPIErrorCode(err))
+			}
 		}
 	}
 }
 func TestCheckAdminRequestAuthType(t *testing.T) {
 	path, err := newTestConfig(globalMinioDefaultRegion)
diff --git a/pkg/hash/reader.go b/pkg/hash/reader.go
index 31981135..4bcd5bad 100644
--- a/pkg/hash/reader.go
+++ b/pkg/hash/reader.go
@@ -59,15 +59,17 @@ func NewReader(src io.Reader, size int64, md5Hex, sha256Hex string) (*Reader, er
 
 	var sha256Hash hash.Hash
 	if len(sha256sum) != 0 {
 		sha256Hash = sha256.New()
 	}
-
+	if size >= 0 {
+		src = io.LimitReader(src, size)
+	}
 	return &Reader{
 		md5sum:     md5sum,
 		sha256sum:  sha256sum,
-		src:        io.LimitReader(src, size),
+		src:        src,
 		size:       size,
 		md5Hash:    md5.New(),
 		sha256Hash: sha256Hash,
 	}, nil
 }

Vulnerable code: 	if r == nil {
		return ErrInternalError
	}

	payload, err := ioutil.ReadAll(r.Body)
	if err != nil {
		logger.LogIf(context.Background(), err)
		return ErrInternalError
	}

	// Populate back the payload.
	r.Body = ioutil.NopCloser(bytes.NewReader(payload))

	// Verify Content-Md5, if payload is set.
	if clntMD5B64, ok := r.Header["Content-Md5"]; ok {
		if clntMD5B64[0] == "" {
		md5Sum, err := base64.StdEncoding.Strict().DecodeString(clntMD5B64[0])
		if err != nil {
			return ErrInvalidDigest
		}
		if !bytes.Equal(md5Sum, getMD5Sum(payload)) {
			return ErrBadDigest
		}
	if skipContentSha256Cksum(r) {
		return ErrNone
	}

	// Verify that X-Amz-Content-Sha256 Header == sha256(payload)
	// If X-Amz-Content-Sha256 header is not sent then we don't calculate/verify sha256(payload)
	sumHex, ok := r.Header["X-Amz-Content-Sha256"]
	if isRequestPresignedSignatureV4(r) {
		sumHex, ok = r.URL.Query()["X-Amz-Content-Sha256"]
	}
	if ok {
		if sumHex[0] == "" {
			return ErrContentSHA256Mismatch
		}
		sum, err := hex.DecodeString(sumHex[0])
		if err != nil {
			return ErrContentSHA256Mismatch
		if !bytes.Equal(sum, getSHA256Sum(payload)) {
		// When request is nil, internal error is returned.
		{nil, ErrInternalError},
	for _, testCase := range testCases {
			t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)

		src:        io.LimitReader(src, size),
Fixed code: 	"github.com/minio/minio/pkg/hash"
	var (
		err                       error
		contentMD5, contentSHA256 []byte
	)
	// Extract 'Content-Md5' if present.
	if _, ok := r.Header["Content-Md5"]; ok {
		contentMD5, err = base64.StdEncoding.Strict().DecodeString(r.Header.Get("Content-Md5"))
		if err != nil || len(contentMD5) == 0 {
	// Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
	// Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
	if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
		if sha256Sum, ok := r.URL.Query()["X-Amz-Content-Sha256"]; ok && len(sha256Sum) > 0 {
			contentSHA256, err = hex.DecodeString(sha256Sum[0])
			if err != nil {
				return ErrContentSHA256Mismatch
			}
	} else if _, ok := r.Header["X-Amz-Content-Sha256"]; !skipSHA256 && ok {
		contentSHA256, err = hex.DecodeString(r.Header.Get("X-Amz-Content-Sha256"))
		if err != nil || len(contentSHA256) == 0 {

	// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
	// The verification happens implicit during reading.
	reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256))
	if err != nil {
		return toAPIErrorCode(err)
	}
	r.Body = ioutil.NopCloser(reader)
	"io/ioutil"
	for i, testCase := range testCases {
			if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(err) != testCase.s3Error {
				t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %d)", i, testCase.s3Error, s3Error, toAPIErrorCode(err))
			}
	if size >= 0 {
		src = io.LimitReader(src, size)
	}
		src:        src,