github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/post-policy_test.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/base64"
    24  	"fmt"
    25  	"io"
    26  	"mime/multipart"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"net/url"
    30  	"strings"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/dustin/go-humanize"
    35  )
    36  
    37  const (
    38  	iso8601DateFormat = "20060102T150405Z"
    39  )
    40  
    41  func newPostPolicyBytesV4WithContentRange(credential, bucketName, objectKey string, expiration time.Time) []byte {
    42  	t := UTCNow()
    43  	// Add the expiration date.
    44  	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat))
    45  	// Add the bucket condition, only accept buckets equal to the one passed.
    46  	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName)
    47  	// Add the key condition, only accept keys equal to the one passed.
    48  	keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey)
    49  	// Add content length condition, only accept content sizes of a given length.
    50  	contentLengthCondStr := `["content-length-range", 1024, 1048576]`
    51  	// Add the algorithm condition, only accept AWS SignV4 Sha256.
    52  	algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]`
    53  	// Add the date condition, only accept the current date.
    54  	dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat))
    55  	// Add the credential string, only accept the credential passed.
    56  	credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential)
    57  	// Add the meta-uuid string, set to 1234
    58  	uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234")
    59  
    60  	// Combine all conditions into one string.
    61  	conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr,
    62  		keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr)
    63  	retStr := "{"
    64  	retStr = retStr + expirationStr + ","
    65  	retStr += conditionStr
    66  	retStr += "}"
    67  
    68  	return []byte(retStr)
    69  }
    70  
    71  // newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches.
    72  func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte {
    73  	t := UTCNow()
    74  	// Add the expiration date.
    75  	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat))
    76  	// Add the bucket condition, only accept buckets equal to the one passed.
    77  	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName)
    78  	// Add the key condition, only accept keys equal to the one passed.
    79  	keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey)
    80  	// Add the algorithm condition, only accept AWS SignV4 Sha256.
    81  	algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]`
    82  	// Add the date condition, only accept the current date.
    83  	dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat))
    84  	// Add the credential string, only accept the credential passed.
    85  	credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential)
    86  	// Add the meta-uuid string, set to 1234
    87  	uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234")
    88  
    89  	// Combine all conditions into one string.
    90  	conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr)
    91  	retStr := "{"
    92  	retStr = retStr + expirationStr + ","
    93  	retStr += conditionStr
    94  	retStr += "}"
    95  
    96  	return []byte(retStr)
    97  }
    98  
    99  // newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches.
   100  func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []byte {
   101  	// Add the expiration date.
   102  	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat))
   103  	// Add the bucket condition, only accept buckets equal to the one passed.
   104  	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName)
   105  	// Add the key condition, only accept keys equal to the one passed.
   106  	keyConditionStr := fmt.Sprintf(`["starts-with", "$key", "%s/upload.txt"]`, objectKey)
   107  
   108  	// Combine all conditions into one string.
   109  	conditionStr := fmt.Sprintf(`"conditions":[%s, %s]`, bucketConditionStr, keyConditionStr)
   110  	retStr := "{"
   111  	retStr = retStr + expirationStr + ","
   112  	retStr += conditionStr
   113  	retStr += "}"
   114  
   115  	return []byte(retStr)
   116  }
   117  
   118  // Wrapper
   119  func TestPostPolicyReservedBucketExploit(t *testing.T) {
   120  	ExecObjectLayerTestWithDirs(t, testPostPolicyReservedBucketExploit)
   121  }
   122  
   123  // testPostPolicyReservedBucketExploit is a test for the exploit fixed in PR
   124  // #16849
   125  func testPostPolicyReservedBucketExploit(obj ObjectLayer, instanceType string, dirs []string, t TestErrHandler) {
   126  	if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
   127  		t.Fatalf("Initializing config.json failed")
   128  	}
   129  
   130  	// Register the API end points with Erasure/FS object layer.
   131  	apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"})
   132  
   133  	credentials := globalActiveCred
   134  	bucketName := minioMetaBucket
   135  	objectName := "config/x"
   136  
   137  	// This exploit needs browser to be enabled.
   138  	if !globalBrowserEnabled {
   139  		globalBrowserEnabled = true
   140  		defer func() { globalBrowserEnabled = false }()
   141  	}
   142  
   143  	// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   144  	rec := httptest.NewRecorder()
   145  	req, perr := newPostRequestV4("", bucketName, objectName, []byte("pwned"), credentials.AccessKey, credentials.SecretKey)
   146  	if perr != nil {
   147  		t.Fatalf("Test %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", instanceType, perr)
   148  	}
   149  
   150  	contentTypeHdr := req.Header.Get("Content-Type")
   151  	contentTypeHdr = strings.Replace(contentTypeHdr, "multipart/form-data", "multipart/form-datA", 1)
   152  	req.Header.Set("Content-Type", contentTypeHdr)
   153  	req.Header.Set("User-Agent", "Mozilla")
   154  
   155  	// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   156  	// Call the ServeHTTP to execute the handler.
   157  	apiRouter.ServeHTTP(rec, req)
   158  
   159  	ctx, cancel := context.WithCancel(GlobalContext)
   160  	defer cancel()
   161  
   162  	// Now check if we actually wrote to backend (regardless of the response
   163  	// returned by the server).
   164  	z := obj.(*erasureServerPools)
   165  	xl := z.serverPools[0].sets[0]
   166  	erasureDisks := xl.getDisks()
   167  	parts, errs := readAllFileInfo(ctx, erasureDisks, "", bucketName, objectName+"/upload.txt", "", false, false)
   168  	for i := range parts {
   169  		if errs[i] == nil {
   170  			if parts[i].Name == objectName+"/upload.txt" {
   171  				t.Errorf("Test %s: Failed to stop post policy handler from writing to minioMetaBucket", instanceType)
   172  			}
   173  		}
   174  	}
   175  }
   176  
   177  // Wrapper for calling TestPostPolicyBucketHandler tests for both Erasure multiple disks and single node setup.
   178  func TestPostPolicyBucketHandler(t *testing.T) {
   179  	ExecObjectLayerTest(t, testPostPolicyBucketHandler)
   180  }
   181  
   182  // testPostPolicyBucketHandler - Tests validate post policy handler uploading objects.
   183  func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
   184  	if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
   185  		t.Fatalf("Initializing config.json failed")
   186  	}
   187  
   188  	// get random bucket name.
   189  	bucketName := getRandomBucketName()
   190  
   191  	var opts ObjectOptions
   192  	// Register the API end points with Erasure/FS object layer.
   193  	apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"})
   194  
   195  	credentials := globalActiveCred
   196  
   197  	curTime := UTCNow()
   198  	curTimePlus5Min := curTime.Add(time.Minute * 5)
   199  
   200  	// bucketnames[0].
   201  	// objectNames[0].
   202  	// uploadIds [0].
   203  	// Create bucket before initiating NewMultipartUpload.
   204  	err := obj.MakeBucket(context.Background(), bucketName, MakeBucketOptions{})
   205  	if err != nil {
   206  		// Failed to create newbucket, abort.
   207  		t.Fatalf("%s : %s", instanceType, err.Error())
   208  	}
   209  
   210  	// Test cases for signature-V2.
   211  	testCasesV2 := []struct {
   212  		expectedStatus int
   213  		accessKey      string
   214  		secretKey      string
   215  	}{
   216  		{http.StatusForbidden, "invalidaccesskey", credentials.SecretKey},
   217  		{http.StatusForbidden, credentials.AccessKey, "invalidsecretkey"},
   218  		{http.StatusNoContent, credentials.AccessKey, credentials.SecretKey},
   219  	}
   220  
   221  	for i, test := range testCasesV2 {
   222  		// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   223  		rec := httptest.NewRecorder()
   224  		req, perr := newPostRequestV2("", bucketName, "testobject", test.accessKey, test.secretKey)
   225  		if perr != nil {
   226  			t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
   227  		}
   228  		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   229  		// Call the ServeHTTP to execute the handler.
   230  		apiRouter.ServeHTTP(rec, req)
   231  		if rec.Code != test.expectedStatus {
   232  			t.Fatalf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, test.expectedStatus, rec.Code)
   233  		}
   234  	}
   235  
   236  	// Test cases for signature-V4.
   237  	testCasesV4 := []struct {
   238  		objectName         string
   239  		data               []byte
   240  		expectedHeaders    map[string]string
   241  		expectedRespStatus int
   242  		accessKey          string
   243  		secretKey          string
   244  		malformedBody      bool
   245  	}{
   246  		// Success case.
   247  		{
   248  			objectName:         "test",
   249  			data:               []byte("Hello, World"),
   250  			expectedRespStatus: http.StatusNoContent,
   251  			expectedHeaders:    map[string]string{"X-Amz-Meta-Uuid": "1234"},
   252  			accessKey:          credentials.AccessKey,
   253  			secretKey:          credentials.SecretKey,
   254  			malformedBody:      false,
   255  		},
   256  		// Bad case invalid request.
   257  		{
   258  			objectName:         "test",
   259  			data:               []byte("Hello, World"),
   260  			expectedRespStatus: http.StatusForbidden,
   261  			accessKey:          "",
   262  			secretKey:          "",
   263  			malformedBody:      false,
   264  		},
   265  		// Bad case malformed input.
   266  		{
   267  			objectName:         "test",
   268  			data:               []byte("Hello, World"),
   269  			expectedRespStatus: http.StatusBadRequest,
   270  			accessKey:          credentials.AccessKey,
   271  			secretKey:          credentials.SecretKey,
   272  			malformedBody:      true,
   273  		},
   274  	}
   275  
   276  	for i, testCase := range testCasesV4 {
   277  		// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   278  		rec := httptest.NewRecorder()
   279  		req, perr := newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey)
   280  		if perr != nil {
   281  			t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
   282  		}
   283  		if testCase.malformedBody {
   284  			// Change the request body.
   285  			req.Body = io.NopCloser(bytes.NewReader([]byte("Hello,")))
   286  		}
   287  		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   288  		// Call the ServeHTTP to execute the handler.
   289  		apiRouter.ServeHTTP(rec, req)
   290  		if rec.Code != testCase.expectedRespStatus {
   291  			t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
   292  		}
   293  		// When the operation is successful, check if sending metadata is successful too
   294  		if rec.Code == http.StatusNoContent {
   295  			objInfo, err := obj.GetObjectInfo(context.Background(), bucketName, testCase.objectName+"/upload.txt", opts)
   296  			if err != nil {
   297  				t.Error("Unexpected error: ", err)
   298  			}
   299  			for k, v := range testCase.expectedHeaders {
   300  				if objInfo.UserDefined[k] != v {
   301  					t.Errorf("Expected to have header %s with value %s, but found value `%s` instead", k, v, objInfo.UserDefined[k])
   302  				}
   303  			}
   304  		}
   305  	}
   306  
   307  	region := "us-east-1"
   308  	// Test cases for signature-V4.
   309  	testCasesV4BadData := []struct {
   310  		objectName         string
   311  		data               []byte
   312  		expectedRespStatus int
   313  		accessKey          string
   314  		secretKey          string
   315  		dates              []interface{}
   316  		policy             string
   317  		noFilename         bool
   318  		corruptedBase64    bool
   319  		corruptedMultipart bool
   320  	}{
   321  		// Success case.
   322  		{
   323  			objectName:         "test",
   324  			data:               []byte("Hello, World"),
   325  			expectedRespStatus: http.StatusNoContent,
   326  			accessKey:          credentials.AccessKey,
   327  			secretKey:          credentials.SecretKey,
   328  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   329  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
   330  		},
   331  		// Success case, no multipart filename.
   332  		{
   333  			objectName:         "test",
   334  			data:               []byte("Hello, World"),
   335  			expectedRespStatus: http.StatusNoContent,
   336  			accessKey:          credentials.AccessKey,
   337  			secretKey:          credentials.SecretKey,
   338  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   339  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
   340  			noFilename:         true,
   341  		},
   342  		// Success case, big body.
   343  		{
   344  			objectName:         "test",
   345  			data:               bytes.Repeat([]byte("a"), 10<<20),
   346  			expectedRespStatus: http.StatusNoContent,
   347  			accessKey:          credentials.AccessKey,
   348  			secretKey:          credentials.SecretKey,
   349  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   350  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
   351  		},
   352  		// Corrupted Base 64 result
   353  		{
   354  			objectName:         "test",
   355  			data:               []byte("Hello, World"),
   356  			expectedRespStatus: http.StatusBadRequest,
   357  			accessKey:          credentials.AccessKey,
   358  			secretKey:          credentials.SecretKey,
   359  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   360  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`,
   361  			corruptedBase64:    true,
   362  		},
   363  		// Corrupted Multipart body
   364  		{
   365  			objectName:         "test",
   366  			data:               []byte("Hello, World"),
   367  			expectedRespStatus: http.StatusBadRequest,
   368  			accessKey:          credentials.AccessKey,
   369  			secretKey:          credentials.SecretKey,
   370  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   371  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`,
   372  			corruptedMultipart: true,
   373  		},
   374  
   375  		// Bad case invalid request.
   376  		{
   377  			objectName:         "test",
   378  			data:               []byte("Hello, World"),
   379  			expectedRespStatus: http.StatusForbidden,
   380  			accessKey:          "",
   381  			secretKey:          "",
   382  			dates:              []interface{}{},
   383  			policy:             ``,
   384  		},
   385  		// Expired document
   386  		{
   387  			objectName:         "test",
   388  			data:               []byte("Hello, World"),
   389  			expectedRespStatus: http.StatusForbidden,
   390  			accessKey:          credentials.AccessKey,
   391  			secretKey:          credentials.SecretKey,
   392  			dates:              []interface{}{curTime.Add(-1 * time.Minute * 5).Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   393  			policy:             `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`,
   394  		},
   395  		// Corrupted policy document
   396  		{
   397  			objectName:         "test",
   398  			data:               []byte("Hello, World"),
   399  			expectedRespStatus: http.StatusForbidden,
   400  			accessKey:          credentials.AccessKey,
   401  			secretKey:          credentials.SecretKey,
   402  			dates:              []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
   403  			policy:             `{"3/aws4_request"]]}`,
   404  		},
   405  	}
   406  
   407  	for i, testCase := range testCasesV4BadData {
   408  		// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   409  		rec := httptest.NewRecorder()
   410  
   411  		testCase.policy = fmt.Sprintf(testCase.policy, testCase.dates...)
   412  
   413  		req, perr := newPostRequestV4Generic("", bucketName, testCase.objectName, testCase.data, testCase.accessKey,
   414  			testCase.secretKey, region, curTime, []byte(testCase.policy), nil, testCase.noFilename, testCase.corruptedBase64, testCase.corruptedMultipart)
   415  		if perr != nil {
   416  			t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
   417  		}
   418  		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   419  		// Call the ServeHTTP to execute the handler.
   420  		apiRouter.ServeHTTP(rec, req)
   421  		if rec.Code != testCase.expectedRespStatus {
   422  			t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
   423  		}
   424  	}
   425  
   426  	testCases2 := []struct {
   427  		objectName          string
   428  		data                []byte
   429  		expectedRespStatus  int
   430  		accessKey           string
   431  		secretKey           string
   432  		malformedBody       bool
   433  		ignoreContentLength bool
   434  	}{
   435  		// Success case.
   436  		{
   437  			objectName:          "test",
   438  			data:                bytes.Repeat([]byte("a"), 1025),
   439  			expectedRespStatus:  http.StatusNoContent,
   440  			accessKey:           credentials.AccessKey,
   441  			secretKey:           credentials.SecretKey,
   442  			malformedBody:       false,
   443  			ignoreContentLength: false,
   444  		},
   445  		// Failed with Content-Length not specified.
   446  		{
   447  			objectName:          "test",
   448  			data:                bytes.Repeat([]byte("a"), 1025),
   449  			expectedRespStatus:  http.StatusNoContent,
   450  			accessKey:           credentials.AccessKey,
   451  			secretKey:           credentials.SecretKey,
   452  			malformedBody:       false,
   453  			ignoreContentLength: true,
   454  		},
   455  		// Failed with entity too small.
   456  		{
   457  			objectName:          "test",
   458  			data:                bytes.Repeat([]byte("a"), 1023),
   459  			expectedRespStatus:  http.StatusBadRequest,
   460  			accessKey:           credentials.AccessKey,
   461  			secretKey:           credentials.SecretKey,
   462  			malformedBody:       false,
   463  			ignoreContentLength: false,
   464  		},
   465  		// Failed with entity too large.
   466  		{
   467  			objectName:          "test",
   468  			data:                bytes.Repeat([]byte("a"), (1*humanize.MiByte)+1),
   469  			expectedRespStatus:  http.StatusBadRequest,
   470  			accessKey:           credentials.AccessKey,
   471  			secretKey:           credentials.SecretKey,
   472  			malformedBody:       false,
   473  			ignoreContentLength: false,
   474  		},
   475  	}
   476  
   477  	for i, testCase := range testCases2 {
   478  		// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   479  		rec := httptest.NewRecorder()
   480  		var req *http.Request
   481  		var perr error
   482  		if testCase.ignoreContentLength {
   483  			req, perr = newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey)
   484  		} else {
   485  			req, perr = newPostRequestV4WithContentLength("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey)
   486  		}
   487  		if perr != nil {
   488  			t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
   489  		}
   490  		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   491  		// Call the ServeHTTP to execute the handler.
   492  		apiRouter.ServeHTTP(rec, req)
   493  		if rec.Code != testCase.expectedRespStatus {
   494  			t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
   495  		}
   496  	}
   497  }
   498  
   499  // Wrapper for calling TestPostPolicyBucketHandlerRedirect tests for both Erasure multiple disks and single node setup.
   500  func TestPostPolicyBucketHandlerRedirect(t *testing.T) {
   501  	ExecObjectLayerTest(t, testPostPolicyBucketHandlerRedirect)
   502  }
   503  
   504  // testPostPolicyBucketHandlerRedirect tests POST Object when success_action_redirect is specified
   505  func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t TestErrHandler) {
   506  	if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
   507  		t.Fatalf("Initializing config.json failed")
   508  	}
   509  
   510  	// get random bucket name.
   511  	bucketName := getRandomBucketName()
   512  
   513  	// Key specified in Form data
   514  	keyName := "test/object"
   515  
   516  	var opts ObjectOptions
   517  
   518  	// The final name of the upload object
   519  	targetObj := keyName + "/upload.txt"
   520  
   521  	// The url of success_action_redirect field
   522  	redirectURL, err := url.Parse("http://www.google.com?query=value")
   523  	if err != nil {
   524  		t.Fatal(err)
   525  	}
   526  
   527  	// Register the API end points with Erasure/FS object layer.
   528  	apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"})
   529  
   530  	credentials := globalActiveCred
   531  
   532  	curTime := UTCNow()
   533  	curTimePlus5Min := curTime.Add(time.Minute * 5)
   534  
   535  	err = obj.MakeBucket(context.Background(), bucketName, MakeBucketOptions{})
   536  	if err != nil {
   537  		// Failed to create newbucket, abort.
   538  		t.Fatalf("%s : %s", instanceType, err.Error())
   539  	}
   540  
   541  	// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
   542  	rec := httptest.NewRecorder()
   543  
   544  	dates := []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}
   545  	policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`
   546  
   547  	// Generate the final policy document
   548  	policy = fmt.Sprintf(policy, dates...)
   549  
   550  	region := "us-east-1"
   551  	// Create a new POST request with success_action_redirect field specified
   552  	req, perr := newPostRequestV4Generic("", bucketName, keyName, []byte("objData"),
   553  		credentials.AccessKey, credentials.SecretKey, region, curTime,
   554  		[]byte(policy), map[string]string{"success_action_redirect": redirectURL.String()}, false, false, false)
   555  
   556  	if perr != nil {
   557  		t.Fatalf("%s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", instanceType, perr)
   558  	}
   559  	// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   560  	// Call the ServeHTTP to execute the handler.
   561  	apiRouter.ServeHTTP(rec, req)
   562  
   563  	// Check the status code, which must be 303 because success_action_redirect is specified
   564  	if rec.Code != http.StatusSeeOther {
   565  		t.Errorf("%s: Expected the response status to be `%d`, but instead found `%d`", instanceType, http.StatusSeeOther, rec.Code)
   566  	}
   567  
   568  	// Get the uploaded object info
   569  	info, err := obj.GetObjectInfo(context.Background(), bucketName, targetObj, opts)
   570  	if err != nil {
   571  		t.Error("Unexpected error: ", err)
   572  	}
   573  
   574  	v := redirectURL.Query()
   575  	v.Add("bucket", info.Bucket)
   576  	v.Add("key", info.Name)
   577  	v.Add("etag", "\""+info.ETag+"\"")
   578  	redirectURL.RawQuery = v.Encode()
   579  	expectedLocation := redirectURL.String()
   580  
   581  	// Check the new location url
   582  	if rec.Header().Get("Location") != expectedLocation {
   583  		t.Errorf("Unexpected location, expected = %s, found = `%s`", rec.Header().Get("Location"), expectedLocation)
   584  	}
   585  }
   586  
   587  // postPresignSignatureV4 - presigned signature for PostPolicy requests.
   588  func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string {
   589  	// Get signining key.
   590  	signingkey := getSigningKey(secretAccessKey, t, location, "s3")
   591  	// Calculate signature.
   592  	signature := getSignature(signingkey, policyBase64)
   593  	return signature
   594  }
   595  
   596  func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secretKey string) (*http.Request, error) {
   597  	// Expire the request five minutes from now.
   598  	expirationTime := UTCNow().Add(time.Minute * 5)
   599  	// Create a new post policy.
   600  	policy := newPostPolicyBytesV2(bucketName, objectName, expirationTime)
   601  	// Only need the encoding.
   602  	encodedPolicy := base64.StdEncoding.EncodeToString(policy)
   603  
   604  	// Presign with V4 signature based on the policy.
   605  	signature := calculateSignatureV2(encodedPolicy, secretKey)
   606  
   607  	formData := map[string]string{
   608  		"AWSAccessKeyId":              accessKey,
   609  		"bucket":                      bucketName,
   610  		"key":                         objectName + "/${filename}",
   611  		"policy":                      encodedPolicy,
   612  		"signature":                   signature,
   613  		"X-Amz-Ignore-signature":      "",
   614  		"X-Amz-Ignore-AWSAccessKeyId": "",
   615  	}
   616  
   617  	// Create the multipart form.
   618  	var buf bytes.Buffer
   619  	w := multipart.NewWriter(&buf)
   620  
   621  	// Set the normal formData
   622  	for k, v := range formData {
   623  		w.WriteField(k, v)
   624  	}
   625  	// Set the File formData
   626  	writer, err := w.CreateFormFile("file", "upload.txt")
   627  	if err != nil {
   628  		// return nil, err
   629  		return nil, err
   630  	}
   631  	writer.Write([]byte("hello world"))
   632  	// Close before creating the new request.
   633  	w.Close()
   634  
   635  	// Set the body equal to the created policy.
   636  	reader := bytes.NewReader(buf.Bytes())
   637  
   638  	req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader)
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  
   643  	// Set form content-type.
   644  	req.Header.Set("Content-Type", w.FormDataContentType())
   645  	return req, nil
   646  }
   647  
   648  func buildGenericPolicy(t time.Time, accessKey, region, bucketName, objectName string, contentLengthRange bool) []byte {
   649  	// Expire the request five minutes from now.
   650  	expirationTime := t.Add(time.Minute * 5)
   651  
   652  	credStr := getCredentialString(accessKey, region, t)
   653  	// Create a new post policy.
   654  	policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime)
   655  	if contentLengthRange {
   656  		policy = newPostPolicyBytesV4WithContentRange(credStr, bucketName, objectName, expirationTime)
   657  	}
   658  	return policy
   659  }
   660  
   661  func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, region string,
   662  	t time.Time, policy []byte, addFormData map[string]string, noFilename bool, corruptedB64 bool, corruptedMultipart bool,
   663  ) (*http.Request, error) {
   664  	// Get the user credential.
   665  	credStr := getCredentialString(accessKey, region, t)
   666  
   667  	// Only need the encoding.
   668  	encodedPolicy := base64.StdEncoding.EncodeToString(policy)
   669  
   670  	if corruptedB64 {
   671  		encodedPolicy = "%!~&" + encodedPolicy
   672  	}
   673  
   674  	// Presign with V4 signature based on the policy.
   675  	signature := postPresignSignatureV4(encodedPolicy, t, secretKey, region)
   676  
   677  	// If there is no filename on multipart, get the filename from the key.
   678  	key := objectName
   679  	if noFilename {
   680  		key += "/upload.txt"
   681  	} else {
   682  		key += "/${filename}"
   683  	}
   684  
   685  	formData := map[string]string{
   686  		"bucket":           bucketName,
   687  		"key":              key,
   688  		"x-amz-credential": credStr,
   689  		"policy":           encodedPolicy,
   690  		"x-amz-signature":  signature,
   691  		"x-amz-date":       t.Format(iso8601DateFormat),
   692  		"x-amz-algorithm":  "AWS4-HMAC-SHA256",
   693  		"x-amz-meta-uuid":  "1234",
   694  		"Content-Encoding": "gzip",
   695  	}
   696  
   697  	// Add form data
   698  	for k, v := range addFormData {
   699  		formData[k] = v
   700  	}
   701  
   702  	// Create the multipart form.
   703  	var buf bytes.Buffer
   704  	w := multipart.NewWriter(&buf)
   705  
   706  	// Set the normal formData
   707  	for k, v := range formData {
   708  		w.WriteField(k, v)
   709  	}
   710  	// Set the File formData but don't if we want send an incomplete multipart request
   711  	if !corruptedMultipart {
   712  		var writer io.Writer
   713  		var err error
   714  		if noFilename {
   715  			writer, err = w.CreateFormField("file")
   716  		} else {
   717  			writer, err = w.CreateFormFile("file", "upload.txt")
   718  		}
   719  		if err != nil {
   720  			// return nil, err
   721  			return nil, err
   722  		}
   723  		writer.Write(objData)
   724  		// Close before creating the new request.
   725  		w.Close()
   726  	}
   727  
   728  	// Set the body equal to the created policy.
   729  	reader := bytes.NewReader(buf.Bytes())
   730  
   731  	req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader)
   732  	if err != nil {
   733  		return nil, err
   734  	}
   735  
   736  	// Set form content-type.
   737  	req.Header.Set("Content-Type", w.FormDataContentType())
   738  	return req, nil
   739  }
   740  
   741  func newPostRequestV4WithContentLength(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) {
   742  	t := UTCNow()
   743  	region := "us-east-1"
   744  	policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, true)
   745  	return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false, false)
   746  }
   747  
   748  func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) {
   749  	t := UTCNow()
   750  	region := "us-east-1"
   751  	policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, false)
   752  	return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false, false)
   753  }