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.
master
Andreas Auernhammer 7 years ago committed by Dee Koder
parent 1cf381f1b0
commit 9c8b7306f5
  1. 69
      cmd/auth-handler.go
  2. 9
      cmd/auth-handler_test.go
  3. 6
      pkg/hash/reader.go

@ -28,6 +28,7 @@ import (
"strings" "strings"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
) )
@ -209,59 +210,45 @@ func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode)
// Verify if request has valid AWS Signature Version '4'. // Verify if request has valid AWS Signature Version '4'.
func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) { func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
if r == nil {
return ErrInternalError
}
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone { if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
return errCode return errCode
} }
payload, err := ioutil.ReadAll(r.Body) var (
if err != nil { err error
logger.LogIf(context.Background(), err) contentMD5, contentSHA256 []byte
return ErrInternalError )
} // Extract 'Content-Md5' if present.
if _, ok := r.Header["Content-Md5"]; ok {
// Populate back the payload. contentMD5, err = base64.StdEncoding.Strict().DecodeString(r.Header.Get("Content-Md5"))
r.Body = ioutil.NopCloser(bytes.NewReader(payload)) if err != nil || len(contentMD5) == 0 {
// Verify Content-Md5, if payload is set.
if clntMD5B64, ok := r.Header["Content-Md5"]; ok {
if clntMD5B64[0] == "" {
return ErrInvalidDigest 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) { // Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
return ErrNone // 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 {
// Verify that X-Amz-Content-Sha256 Header == sha256(payload) contentSHA256, err = hex.DecodeString(sha256Sum[0])
// If X-Amz-Content-Sha256 header is not sent then we don't calculate/verify sha256(payload) if err != nil {
sumHex, ok := r.Header["X-Amz-Content-Sha256"] return ErrContentSHA256Mismatch
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)) { } 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 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 return ErrNone
} }

@ -19,6 +19,7 @@ package cmd
import ( import (
"bytes" "bytes"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -361,8 +362,6 @@ func TestIsReqAuthenticated(t *testing.T) {
req *http.Request req *http.Request
s3Error APIErrorCode s3Error APIErrorCode
}{ }{
// When request is nil, internal error is returned.
{nil, ErrInternalError},
// When request is unsigned, access denied is returned. // When request is unsigned, access denied is returned.
{mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied}, {mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
// Empty Content-Md5 header. // Empty Content-Md5 header.
@ -376,9 +375,11 @@ func TestIsReqAuthenticated(t *testing.T) {
} }
// Validates all testcases. // Validates all testcases.
for _, testCase := range testCases { for i, testCase := range testCases {
if s3Error := isReqAuthenticated(testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error { 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))
}
} }
} }
} }

@ -61,11 +61,13 @@ func NewReader(src io.Reader, size int64, md5Hex, sha256Hex string) (*Reader, er
if len(sha256sum) != 0 { if len(sha256sum) != 0 {
sha256Hash = sha256.New() sha256Hash = sha256.New()
} }
if size >= 0 {
src = io.LimitReader(src, size)
}
return &Reader{ return &Reader{
md5sum: md5sum, md5sum: md5sum,
sha256sum: sha256sum, sha256sum: sha256sum,
src: io.LimitReader(src, size), src: src,
size: size, size: size,
md5Hash: md5.New(), md5Hash: md5.New(),
sha256Hash: sha256Hash, sha256Hash: sha256Hash,

Loading…
Cancel
Save